From 3de80ea0d4648dc0a4eb2d8675897aa4458efce5 Mon Sep 17 00:00:00 2001 From: Joshua Carter Date: Fri, 18 Nov 2022 10:19:11 -0800 Subject: [PATCH] Feature: refactor service worker (#41) * Improve console logs on peer:connected event (#40) * SAM-64 high level modularization (#44) * Rename service-worker.ts to worker-messaging.ts * Exclude self from 'no-restricted-globals' eslint rule * Modularize worker/index.ts and replace with new root worker/service-worker.ts file * Add missing message handlers to new refactored code * Add trace logging for forwarding fetch requests * Format logger output * Create new DevTools util accessible via 'SamizdAppDevTools' * Return root logger from logging.getLoggers() * Write boilerplate tests for new worker * SAM-66 refactor p2p client (#45) * Add localforage to SamizdAppDevTools * Add workbox logging comment to root service-worker.ts * Refactor p2p-client into high-level OOP architecture * proper agentversion (#48) * Improve worker logging (#49) * Don't use logger namespaces as logger names (makes name matching easier) * Prefix log with method name, not logLevel setting * Don't log using console.trace() * Persist loglevel using localforage and correctly set default levels * Move logging module to separate directory * Set prettier tabWidth to 2 for yaml and .prettierrc files * Create new logging.yaml file that sets default levels on all matched loggers * Handle possibility of no logging config * Fix: set log level of worker/p2p/bootstrap to INFO * SAM-68 better libp2p implementation (#51) * Uninstall multiaddr package (use the same one libp2p uses instead) * Delete old worker/index.ts file (uninstalling old package caused import errors) * Refactor bootstrap-list to more cleanly manage addresses and configure libp2p discovery * Refactor stream-factory with cleaner logic and more robust restart * Adjust p2p logging * Import workbox-precaching for types (preivously was imported in index.ts) * Add new SamizdAppDevTools.addressBook property * Improve p2p logging * Disable autodial and restart() and replace with serve connection retry logic * SAM-73 don't cache empty p2p bootstrap list (#53) Don't cache empty p2p bootstrap list * SAM-71 migrations (with libp2p migration) (#52) * Make JSON parsing more defensive in bootstrap-list loadCache() * Create new migrations layer and add first migration: libp2p.bootstrap -> p2p.bootstrap-list * SAM-72 close stats socket (#54) Close stats websocket after collection * SAM-65 disable static cache when running locally (#55) Disable static cache when running locally Co-authored-by: Ryan Bennett --- .prettierrc | 2 +- package-lock.json | 483 ++----- package.json | 5 +- packages/gateway-client/.eslintrc.js | 24 + packages/gateway-client/.eslintrc.json | 18 - packages/gateway-client/project.json | 1 + .../src/app/components/home/home.tsx | 2 +- .../src/app/components/home/status.tsx | 2 +- .../service-worker/serviceWorker.logic.ts | 2 +- .../service-worker/serviceWorker.slice.ts | 2 +- ...{service-worker.ts => worker-messaging.ts} | 0 .../gateway-client/src/worker/bootstrap.ts | 64 + .../gateway-client/src/worker/client.spec.ts | 101 ++ packages/gateway-client/src/worker/client.ts | 21 + .../src/worker/devtools/index.ts | 33 + .../fetch-handlers/fetch-handlers.spec.ts | 11 + .../worker/fetch-handlers/fetch-handlers.ts | 44 + .../src/worker/fetch-handlers/index.ts | 4 + .../pass-through-handler.spec.ts | 18 + .../fetch-handlers/pass-through-handler.ts | 7 + .../pleroma-timeline-handler.spec.ts | 33 + .../pleroma-timeline-handler.ts | 21 + .../static-cache-handler.spec.ts | 29 + .../fetch-handlers/static-cache-handler.ts | 60 + packages/gateway-client/src/worker/index.ts | 1150 ----------------- .../src/worker/logging/index.ts | 115 ++ .../src/worker/logging/logging.spec.ts | 66 + .../src/worker/logging/logging.yaml | 5 + .../src/worker/logging/yaml.d.ts | 4 + .../src/worker/messenger.spec.ts | 26 + .../gateway-client/src/worker/messenger.ts | 57 + .../src/worker/migrations.spec.ts | 7 + .../gateway-client/src/worker/migrations.ts | 100 ++ .../src/worker/p2p-client/bootstrap-list.ts | 427 ++++++ .../src/worker/p2p-client/index.ts | 305 +++++ .../src/worker/p2p-client/libp2p-logging.ts | 41 + .../src/worker/p2p-client/stream-factory.ts | 145 +++ .../src/worker/p2p-fetch/override-fetch.ts | 306 +++++ .../src/worker/service-worker.ts | 45 + .../gateway-client/src/worker/status.spec.ts | 7 + packages/gateway-client/src/worker/status.ts | 53 + packages/gateway-client/webpack.config.js | 6 +- 42 files changed, 2284 insertions(+), 1568 deletions(-) create mode 100644 packages/gateway-client/.eslintrc.js delete mode 100644 packages/gateway-client/.eslintrc.json rename packages/gateway-client/src/{service-worker.ts => worker-messaging.ts} (100%) create mode 100644 packages/gateway-client/src/worker/bootstrap.ts create mode 100644 packages/gateway-client/src/worker/client.spec.ts create mode 100644 packages/gateway-client/src/worker/client.ts create mode 100644 packages/gateway-client/src/worker/devtools/index.ts create mode 100644 packages/gateway-client/src/worker/fetch-handlers/fetch-handlers.spec.ts create mode 100644 packages/gateway-client/src/worker/fetch-handlers/fetch-handlers.ts create mode 100644 packages/gateway-client/src/worker/fetch-handlers/index.ts create mode 100644 packages/gateway-client/src/worker/fetch-handlers/pass-through-handler.spec.ts create mode 100644 packages/gateway-client/src/worker/fetch-handlers/pass-through-handler.ts create mode 100644 packages/gateway-client/src/worker/fetch-handlers/pleroma-timeline-handler.spec.ts create mode 100644 packages/gateway-client/src/worker/fetch-handlers/pleroma-timeline-handler.ts create mode 100644 packages/gateway-client/src/worker/fetch-handlers/static-cache-handler.spec.ts create mode 100644 packages/gateway-client/src/worker/fetch-handlers/static-cache-handler.ts delete mode 100644 packages/gateway-client/src/worker/index.ts create mode 100644 packages/gateway-client/src/worker/logging/index.ts create mode 100644 packages/gateway-client/src/worker/logging/logging.spec.ts create mode 100644 packages/gateway-client/src/worker/logging/logging.yaml create mode 100644 packages/gateway-client/src/worker/logging/yaml.d.ts create mode 100644 packages/gateway-client/src/worker/messenger.spec.ts create mode 100644 packages/gateway-client/src/worker/messenger.ts create mode 100644 packages/gateway-client/src/worker/migrations.spec.ts create mode 100644 packages/gateway-client/src/worker/migrations.ts create mode 100644 packages/gateway-client/src/worker/p2p-client/bootstrap-list.ts create mode 100644 packages/gateway-client/src/worker/p2p-client/index.ts create mode 100644 packages/gateway-client/src/worker/p2p-client/libp2p-logging.ts create mode 100644 packages/gateway-client/src/worker/p2p-client/stream-factory.ts create mode 100644 packages/gateway-client/src/worker/p2p-fetch/override-fetch.ts create mode 100644 packages/gateway-client/src/worker/service-worker.ts create mode 100644 packages/gateway-client/src/worker/status.spec.ts create mode 100644 packages/gateway-client/src/worker/status.ts diff --git a/.prettierrc b/.prettierrc index 1a7f5ce..b19eceb 100644 --- a/.prettierrc +++ b/.prettierrc @@ -6,7 +6,7 @@ "tabWidth": 4, "overrides": [ { - "files": "*.json", + "files": ["*.json", "*.yaml", "*.prettierrc"], "options": { "tabWidth": 2 } diff --git a/package-lock.json b/package-lock.json index 23bf802..fbbec54 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,10 +33,11 @@ "core-js": "^3.6.5", "datastore-level": "^9.0.1", "it-pipe": "^2.0.4", + "js-yaml": "^4.1.0", "libp2p": "^0.39.5", "lob-enc": "^0.0.17", "localforage": "^1.10.0", - "multiaddr": "^10.0.1", + "loglevel": "^1.8.1", "platform": "^1.3.6", "react": "18.2.0", "react-archer": "^4.1.0", @@ -67,6 +68,7 @@ "@nrwl/workspace": "14.5.10", "@testing-library/react": "13.3.0", "@types/jest": "27.4.1", + "@types/js-yaml": "^4.0.5", "@types/node": "^16.11.7", "@types/platform": "^1.3.4", "@types/react": "18.0.17", @@ -78,6 +80,7 @@ "@typescript-eslint/parser": "^5.29.0", "babel-jest": "27.5.1", "babel-plugin-styled-components": "1.10.7", + "confusing-browser-globals": "^1.0.11", "cypress": "^10.2.0", "eslint": "~8.15.0", "eslint-config-prettier": "8.1.0", @@ -2620,12 +2623,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/@eslint/eslintrc/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, "node_modules/@eslint/eslintrc/node_modules/globals": { "version": "13.17.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.17.0.tgz", @@ -2641,18 +2638,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@eslint/eslintrc/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, "node_modules/@eslint/eslintrc/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -2713,6 +2698,28 @@ "node": ">=8" } }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/@istanbuljs/schema": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", @@ -5581,12 +5588,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/@nrwl/webpack/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, "node_modules/@nrwl/webpack/node_modules/babel-jest": { "version": "28.1.3", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-28.1.3.tgz", @@ -6391,18 +6392,6 @@ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" } }, - "node_modules/@nrwl/webpack/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, "node_modules/@nrwl/webpack/node_modules/nx": { "version": "14.7.6", "resolved": "https://registry.npmjs.org/nx/-/nx-14.7.6.tgz", @@ -7824,6 +7813,12 @@ "pretty-format": "^27.0.0" } }, + "node_modules/@types/js-yaml": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.5.tgz", + "integrity": "sha512-FhpRzf927MNQdRZP0J5DLIdTXhjLYzeUTmLAu69mnVksLH9CJY3IuSeEgbKUki7GQZm0WqDkGzyxju2EZGD2wA==", + "dev": true + }, "node_modules/@types/json-schema": { "version": "7.0.11", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", @@ -8837,13 +8832,9 @@ "dev": true }, "node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "dependencies": { - "sprintf-js": "~1.0.2" - } + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "node_modules/aria-query": { "version": "5.0.2", @@ -11350,15 +11341,6 @@ "node": ">=0.10" } }, - "node_modules/data-uri-to-buffer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.0.tgz", - "integrity": "sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA==", - "peer": true, - "engines": { - "node": ">= 12" - } - }, "node_modules/data-urls": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz", @@ -12476,12 +12458,6 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "node_modules/eslint/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, "node_modules/eslint/node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -12534,18 +12510,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, "node_modules/eslint/node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -13019,29 +12983,6 @@ "pend": "~1.2.0" } }, - "node_modules/fetch-blob": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "paypal", - "url": "https://paypal.me/jimmywarting" - } - ], - "peer": true, - "dependencies": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" - }, - "engines": { - "node": "^12.20 || >= 14.13" - } - }, "node_modules/figures": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", @@ -13373,18 +13314,6 @@ "node": ">= 6" } }, - "node_modules/formdata-polyfill": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", - "peer": true, - "dependencies": { - "fetch-blob": "^3.1.2" - }, - "engines": { - "node": ">=12.20.0" - } - }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -15864,13 +15793,11 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dev": true, + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" + "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" @@ -16675,6 +16602,18 @@ "node": ">=8" } }, + "node_modules/loglevel": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.8.1.tgz", + "integrity": "sha512-tCRIJM51SHjAayKwC+QAg8hT8vg6z7GSgLJKGvzuPb1Wc+hLzqtuVLxp6/HzSPOozuK+8ErAhy7U/sVzw8Dgfg==", + "engines": { + "node": ">= 0.6.0" + }, + "funding": { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/loglevel" + } + }, "node_modules/long": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/long/-/long-5.2.0.tgz", @@ -17140,56 +17079,6 @@ "node": ">= 6.0.0" } }, - "node_modules/multiaddr": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/multiaddr/-/multiaddr-10.0.1.tgz", - "integrity": "sha512-G5upNcGzEGuTHkzxezPrrD6CaIHR9uo+7MwqhNVcXTs33IInon4y7nMiGxl2CY5hG7chvYQUQhz5V52/Qe3cbg==", - "dependencies": { - "dns-over-http-resolver": "^1.2.3", - "err-code": "^3.0.1", - "is-ip": "^3.1.0", - "multiformats": "^9.4.5", - "uint8arrays": "^3.0.0", - "varint": "^6.0.0" - } - }, - "node_modules/multiaddr/node_modules/dns-over-http-resolver": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/dns-over-http-resolver/-/dns-over-http-resolver-1.2.3.tgz", - "integrity": "sha512-miDiVSI6KSNbi4SVifzO/reD8rMnxgrlnkrlkugOLQpWQTe2qMdHsZp5DmfKjxNE+/T3VAAYLQUZMv9SMr6+AA==", - "dependencies": { - "debug": "^4.3.1", - "native-fetch": "^3.0.0", - "receptacle": "^1.3.2" - } - }, - "node_modules/multiaddr/node_modules/ip-regex": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.3.0.tgz", - "integrity": "sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==", - "engines": { - "node": ">=8" - } - }, - "node_modules/multiaddr/node_modules/is-ip": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-ip/-/is-ip-3.1.0.tgz", - "integrity": "sha512-35vd5necO7IitFPjd/YBeqwWnyDWbuLH9ZXQdMfDA8TEo7pv5X8yfrvVO3xbJbLUlERCMvf6X0hTUamQxCYJ9Q==", - "dependencies": { - "ip-regex": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/multiaddr/node_modules/native-fetch": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/native-fetch/-/native-fetch-3.0.0.tgz", - "integrity": "sha512-G3Z7vx0IFb/FQ4JxvtqGABsOTIqRWvgQz6e+erkB+JJD6LrszQtMozEHI4EkmgZQvnGHrpLVzUWk7t4sJCIkVw==", - "peerDependencies": { - "node-fetch": "*" - } - }, "node_modules/multicast-dns": { "version": "7.2.5", "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", @@ -17288,43 +17177,6 @@ "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", "dev": true }, - "node_modules/node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "github", - "url": "https://paypal.me/jimmywarting" - } - ], - "peer": true, - "engines": { - "node": ">=10.5.0" - } - }, - "node_modules/node-fetch": { - "version": "3.2.10", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.2.10.tgz", - "integrity": "sha512-MhuzNwdURnZ1Cp4XTazr69K0BTizsBroX7Zx3UgDSVcZYKF/6p0CBe4EUb/hLqmzVhl0UpYfgRljQ4yxE+iCxA==", - "peer": true, - "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" - } - }, "node_modules/node-forge": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", @@ -17530,24 +17382,6 @@ } } }, - "node_modules/nx/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "node_modules/nx/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -22261,15 +22095,6 @@ "defaults": "^1.0.3" } }, - "node_modules/web-streams-polyfill": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", - "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==", - "peer": true, - "engines": { - "node": ">= 8" - } - }, "node_modules/webidl-conversions": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", @@ -25096,12 +24921,6 @@ "strip-json-comments": "^3.1.1" }, "dependencies": { - "argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, "globals": { "version": "13.17.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.17.0.tgz", @@ -25111,15 +24930,6 @@ "type-fest": "^0.20.2" } }, - "js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "requires": { - "argparse": "^2.0.1" - } - }, "minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -25165,6 +24975,27 @@ "get-package-type": "^0.1.0", "js-yaml": "^3.13.1", "resolve-from": "^5.0.0" + }, + "dependencies": { + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + } } }, "@istanbuljs/schema": { @@ -27370,12 +27201,6 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true }, - "argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, "babel-jest": { "version": "28.1.3", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-28.1.3.tgz", @@ -28037,15 +27862,6 @@ "supports-color": "^8.0.0" } }, - "js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "requires": { - "argparse": "^2.0.1" - } - }, "nx": { "version": "14.7.6", "resolved": "https://registry.npmjs.org/nx/-/nx-14.7.6.tgz", @@ -29126,6 +28942,12 @@ "pretty-format": "^27.0.0" } }, + "@types/js-yaml": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.5.tgz", + "integrity": "sha512-FhpRzf927MNQdRZP0J5DLIdTXhjLYzeUTmLAu69mnVksLH9CJY3IuSeEgbKUki7GQZm0WqDkGzyxju2EZGD2wA==", + "dev": true + }, "@types/json-schema": { "version": "7.0.11", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", @@ -29928,13 +29750,9 @@ "dev": true }, "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "requires": { - "sprintf-js": "~1.0.2" - } + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "aria-query": { "version": "5.0.2", @@ -31833,12 +31651,6 @@ "assert-plus": "^1.0.0" } }, - "data-uri-to-buffer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.0.tgz", - "integrity": "sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA==", - "peer": true - }, "data-urls": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz", @@ -32437,12 +32249,6 @@ "v8-compile-cache": "^2.0.3" }, "dependencies": { - "argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, "escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -32477,15 +32283,6 @@ "type-fest": "^0.20.2" } }, - "js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "requires": { - "argparse": "^2.0.1" - } - }, "levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -33129,16 +32926,6 @@ "pend": "~1.2.0" } }, - "fetch-blob": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "peer": true, - "requires": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" - } - }, "figures": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", @@ -33380,15 +33167,6 @@ "mime-types": "^2.1.12" } }, - "formdata-polyfill": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", - "peer": true, - "requires": { - "fetch-blob": "^3.1.2" - } - }, "forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -35230,13 +35008,11 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dev": true, + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" + "argparse": "^2.0.1" } }, "jsbn": { @@ -35863,6 +35639,11 @@ } } }, + "loglevel": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.8.1.tgz", + "integrity": "sha512-tCRIJM51SHjAayKwC+QAg8hT8vg6z7GSgLJKGvzuPb1Wc+hLzqtuVLxp6/HzSPOozuK+8ErAhy7U/sVzw8Dgfg==" + }, "long": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/long/-/long-5.2.0.tgz", @@ -36212,50 +35993,6 @@ "xtend": "^4.0.0" } }, - "multiaddr": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/multiaddr/-/multiaddr-10.0.1.tgz", - "integrity": "sha512-G5upNcGzEGuTHkzxezPrrD6CaIHR9uo+7MwqhNVcXTs33IInon4y7nMiGxl2CY5hG7chvYQUQhz5V52/Qe3cbg==", - "requires": { - "dns-over-http-resolver": "^1.2.3", - "err-code": "^3.0.1", - "is-ip": "^3.1.0", - "multiformats": "^9.4.5", - "uint8arrays": "^3.0.0", - "varint": "^6.0.0" - }, - "dependencies": { - "dns-over-http-resolver": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/dns-over-http-resolver/-/dns-over-http-resolver-1.2.3.tgz", - "integrity": "sha512-miDiVSI6KSNbi4SVifzO/reD8rMnxgrlnkrlkugOLQpWQTe2qMdHsZp5DmfKjxNE+/T3VAAYLQUZMv9SMr6+AA==", - "requires": { - "debug": "^4.3.1", - "native-fetch": "^3.0.0", - "receptacle": "^1.3.2" - } - }, - "ip-regex": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.3.0.tgz", - "integrity": "sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==" - }, - "is-ip": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-ip/-/is-ip-3.1.0.tgz", - "integrity": "sha512-35vd5necO7IitFPjd/YBeqwWnyDWbuLH9ZXQdMfDA8TEo7pv5X8yfrvVO3xbJbLUlERCMvf6X0hTUamQxCYJ9Q==", - "requires": { - "ip-regex": "^4.0.0" - } - }, - "native-fetch": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/native-fetch/-/native-fetch-3.0.0.tgz", - "integrity": "sha512-G3Z7vx0IFb/FQ4JxvtqGABsOTIqRWvgQz6e+erkB+JJD6LrszQtMozEHI4EkmgZQvnGHrpLVzUWk7t4sJCIkVw==", - "requires": {} - } - } - }, "multicast-dns": { "version": "7.2.5", "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", @@ -36333,23 +36070,6 @@ "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", "dev": true }, - "node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "peer": true - }, - "node-fetch": { - "version": "3.2.10", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.2.10.tgz", - "integrity": "sha512-MhuzNwdURnZ1Cp4XTazr69K0BTizsBroX7Zx3UgDSVcZYKF/6p0CBe4EUb/hLqmzVhl0UpYfgRljQ4yxE+iCxA==", - "peer": true, - "requires": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - } - }, "node-forge": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", @@ -36498,23 +36218,6 @@ "v8-compile-cache": "2.3.0", "yargs": "^17.4.0", "yargs-parser": "21.0.1" - }, - "dependencies": { - "argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "requires": { - "argparse": "^2.0.1" - } - } } }, "object-assign": { @@ -39974,12 +39677,6 @@ "defaults": "^1.0.3" } }, - "web-streams-polyfill": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", - "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==", - "peer": true - }, "webidl-conversions": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", diff --git a/package.json b/package.json index 9eb7e41..08ab8a2 100644 --- a/package.json +++ b/package.json @@ -34,10 +34,11 @@ "core-js": "^3.6.5", "datastore-level": "^9.0.1", "it-pipe": "^2.0.4", + "js-yaml": "^4.1.0", "libp2p": "^0.39.5", "lob-enc": "^0.0.17", "localforage": "^1.10.0", - "multiaddr": "^10.0.1", + "loglevel": "^1.8.1", "platform": "^1.3.6", "react": "18.2.0", "react-archer": "^4.1.0", @@ -68,6 +69,7 @@ "@nrwl/workspace": "14.5.10", "@testing-library/react": "13.3.0", "@types/jest": "27.4.1", + "@types/js-yaml": "^4.0.5", "@types/node": "^16.11.7", "@types/platform": "^1.3.4", "@types/react": "18.0.17", @@ -79,6 +81,7 @@ "@typescript-eslint/parser": "^5.29.0", "babel-jest": "27.5.1", "babel-plugin-styled-components": "1.10.7", + "confusing-browser-globals": "^1.0.11", "cypress": "^10.2.0", "eslint": "~8.15.0", "eslint-config-prettier": "8.1.0", diff --git a/packages/gateway-client/.eslintrc.js b/packages/gateway-client/.eslintrc.js new file mode 100644 index 0000000..6e84a9e --- /dev/null +++ b/packages/gateway-client/.eslintrc.js @@ -0,0 +1,24 @@ +const restrictedGlobals = require('confusing-browser-globals'); + +module.exports = { + extends: ['plugin:@nrwl/nx/react', '../../.eslintrc.json'], + ignorePatterns: ['!**/*'], + overrides: [ + { + files: ['*.ts', '*.tsx', '*.js', '*.jsx'], + rules: { + 'no-restricted-globals': ['error'].concat( + restrictedGlobals.filter(global => global !== 'self') + ), + }, + }, + { + files: ['*.ts', '*.tsx'], + rules: {}, + }, + { + files: ['*.js', '*.jsx'], + rules: {}, + }, + ], +}; diff --git a/packages/gateway-client/.eslintrc.json b/packages/gateway-client/.eslintrc.json deleted file mode 100644 index 734ddac..0000000 --- a/packages/gateway-client/.eslintrc.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "extends": ["plugin:@nrwl/nx/react", "../../.eslintrc.json"], - "ignorePatterns": ["!**/*"], - "overrides": [ - { - "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], - "rules": {} - }, - { - "files": ["*.ts", "*.tsx"], - "rules": {} - }, - { - "files": ["*.js", "*.jsx"], - "rules": {} - } - ] -} diff --git a/packages/gateway-client/project.json b/packages/gateway-client/project.json index 939bee1..d3baa2b 100644 --- a/packages/gateway-client/project.json +++ b/packages/gateway-client/project.json @@ -68,6 +68,7 @@ "executor": "@nrwl/linter:eslint", "outputs": ["{options.outputFile}"], "options": { + "eslintConfig": "packages/gateway-client/.eslintrc.js", "lintFilePatterns": ["packages/gateway-client/**/*.{ts,tsx,js,jsx}"] } }, diff --git a/packages/gateway-client/src/app/components/home/home.tsx b/packages/gateway-client/src/app/components/home/home.tsx index de40155..115081c 100644 --- a/packages/gateway-client/src/app/components/home/home.tsx +++ b/packages/gateway-client/src/app/components/home/home.tsx @@ -6,7 +6,7 @@ import { useCallback, useEffect, useState } from 'react'; import { useSelector } from 'react-redux'; import styled from 'styled-components'; -import { ClientMessageType, ServerPeerStatus } from '../../../service-worker'; +import { ClientMessageType, ServerPeerStatus } from '../../../worker-messaging'; import { selectWorkerStatus } from '../../redux/service-worker/serviceWorker.slice'; import { getSupportedPlatform, diff --git a/packages/gateway-client/src/app/components/home/status.tsx b/packages/gateway-client/src/app/components/home/status.tsx index c2f6a5b..a34e916 100644 --- a/packages/gateway-client/src/app/components/home/status.tsx +++ b/packages/gateway-client/src/app/components/home/status.tsx @@ -6,7 +6,7 @@ import { useSelector } from 'react-redux'; import styled from 'styled-components'; import { Link } from 'react-router-dom'; -import { ServerPeerStatus } from '../../../service-worker'; +import { ServerPeerStatus } from '../../../worker-messaging'; import { selectRelayAddresses, selectWorkerStatus, diff --git a/packages/gateway-client/src/app/redux/service-worker/serviceWorker.logic.ts b/packages/gateway-client/src/app/redux/service-worker/serviceWorker.logic.ts index 2a31471..b58163b 100644 --- a/packages/gateway-client/src/app/redux/service-worker/serviceWorker.logic.ts +++ b/packages/gateway-client/src/app/redux/service-worker/serviceWorker.logic.ts @@ -5,7 +5,7 @@ import { Message, ServerPeerStatus, WorkerMessageType, -} from '../../../service-worker'; +} from '../../../worker-messaging'; import { AppDispatch } from '../store'; import { setIsControlling, diff --git a/packages/gateway-client/src/app/redux/service-worker/serviceWorker.slice.ts b/packages/gateway-client/src/app/redux/service-worker/serviceWorker.slice.ts index 48e10cf..06c07f0 100644 --- a/packages/gateway-client/src/app/redux/service-worker/serviceWorker.slice.ts +++ b/packages/gateway-client/src/app/redux/service-worker/serviceWorker.slice.ts @@ -1,5 +1,5 @@ import { createSelector, createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { ServerPeerStatus } from '../../../service-worker'; +import { ServerPeerStatus } from '../../../worker-messaging'; import { RootState } from '../store'; diff --git a/packages/gateway-client/src/service-worker.ts b/packages/gateway-client/src/worker-messaging.ts similarity index 100% rename from packages/gateway-client/src/service-worker.ts rename to packages/gateway-client/src/worker-messaging.ts diff --git a/packages/gateway-client/src/worker/bootstrap.ts b/packages/gateway-client/src/worker/bootstrap.ts new file mode 100644 index 0000000..b55c994 --- /dev/null +++ b/packages/gateway-client/src/worker/bootstrap.ts @@ -0,0 +1,64 @@ +import localforage from 'localforage'; + +import { ClientMessageType } from '../worker-messaging'; +import { SamizdAppDevTools } from './devtools'; +import { logger } from './logging'; +import messenger from './messenger'; +import { runMigrations } from './migrations'; +import { P2pClient } from './p2p-client'; +import { overrideFetch } from './p2p-fetch/override-fetch'; +import status from './status'; + +declare const self: ServiceWorkerGlobalScope; + +export default async () => { + // create our bootstrap logger + const log = logger.getLogger('worker/bootstrap'); + + // run migrations before anything else + await runMigrations(); + + // setup event handlers + self.addEventListener('online', () => log.debug('<<< log.debug('<<< { + log.info('Installing...'); + + // The promise that skipWaiting() returns can be safely ignored. + self.skipWaiting(); + + // Perform any other actions required for your + // service worker to install, potentially inside + // of event.waitUntil(); + log.debug('Skipped waiting'); + }); + + self.addEventListener('activate', async _event => { + log.info('Activating...'); + + await self.clients.claim(); + + // send status update to our client + status.sendCurrent(); + + log.debug('Finish clients claim'); + }); + + messenger.addListener(ClientMessageType.OPENED, () => { + localforage.setItem('started', { started: true }); + }); + + // init messenger + messenger.init(); + + // create and start p2p client + const p2pClient = new P2pClient(); + p2pClient.start(); + + // initialize fetch override + overrideFetch(p2pClient); + + // create dev tools + new SamizdAppDevTools(p2pClient); +}; diff --git a/packages/gateway-client/src/worker/client.spec.ts b/packages/gateway-client/src/worker/client.spec.ts new file mode 100644 index 0000000..33da64d --- /dev/null +++ b/packages/gateway-client/src/worker/client.spec.ts @@ -0,0 +1,101 @@ +import { + isBootstrapAppUrl, + getBootstrapClient, + getClient, + getWindowClient, +} from './client'; + +declare const global: typeof globalThis & ServiceWorkerGlobalScope; + +describe('client should', () => { + let mockResult = [] as Client[]; + + beforeEach(() => { + mockResult = []; + + Object.defineProperty(global, 'clients', { + configurable: true, + value: { + matchAll: jest.fn(() => Promise.resolve(mockResult)), + } as unknown as Clients, + }); + + global.WindowClient = global.Client = class { + public url = + 'http://example.com/api/v1/timelines/public?local=true'; + } as typeof Client as typeof WindowClient; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('be initialized', () => { + expect(getBootstrapClient).toBeDefined(); + expect(getClient).toBeDefined(); + expect(getWindowClient).toBeDefined(); + expect(isBootstrapAppUrl).toBeDefined(); + }); + + it('isBootstrapAppUrl should return true for bootstrap app url', () => { + expect( + isBootstrapAppUrl(new URL('https://example.com/smz/pwa')) + ).toBeTruthy(); + }); + + it('isBootstrapAppUrl should return false for non-bootstrap app url', () => { + expect(isBootstrapAppUrl(new URL('https://example.com'))).toBeFalsy(); + }); + + it('getBootstrapClient should return bootstrap client', async () => { + const mockClient = new WindowClient(); + Object.defineProperty(mockClient, 'url', { + configurable: true, + value: 'https://example.com/smz/pwa', + }); + + mockResult = [mockClient]; + + const client = await getBootstrapClient(); + + expect(client).toBe(mockClient); + }); + + it('getBootstrapClient should return undefined if no bootstrap client', async () => { + const client = await getBootstrapClient(); + + expect(client).toBeUndefined(); + }); + + it('getClient should return client', async () => { + const mockClient = new WindowClient(); + + mockResult = [mockClient]; + + const client = await getClient(() => true); + + expect(client).toBe(mockClient); + }); + + it('getClient should return undefined if no client', async () => { + const client = await getClient(() => true); + + expect(client).toBeUndefined(); + }); + + it('getWindowClient should return window client', async () => { + const mockClient = new WindowClient(); + + mockResult = [mockClient]; + + const client = await getWindowClient(() => true); + + expect(client).toBe(mockClient); + }); + + it('getWindowClient should return undefined if no window client', async () => { + const client = await getWindowClient(() => true); + + expect(client).toBeUndefined(); + }); +}); diff --git a/packages/gateway-client/src/worker/client.ts b/packages/gateway-client/src/worker/client.ts new file mode 100644 index 0000000..8e2105e --- /dev/null +++ b/packages/gateway-client/src/worker/client.ts @@ -0,0 +1,21 @@ +declare const self: ServiceWorkerGlobalScope; + +export const isBootstrapAppUrl = (url: URL): boolean => + url.pathname.startsWith('/smz/pwa'); + +export const getClient = async (matcher: (client: Client) => boolean) => { + const allClients = await self.clients.matchAll(); + return allClients.find(matcher); +}; + +export const getWindowClient = async ( + matcher: (client: WindowClient) => boolean +) => { + const client = (await getClient( + it => it instanceof WindowClient && matcher(it) + )) as WindowClient | undefined; + return client; +}; + +export const getBootstrapClient = () => + getWindowClient(it => isBootstrapAppUrl(new URL(it.url))); diff --git a/packages/gateway-client/src/worker/devtools/index.ts b/packages/gateway-client/src/worker/devtools/index.ts new file mode 100644 index 0000000..7bd3130 --- /dev/null +++ b/packages/gateway-client/src/worker/devtools/index.ts @@ -0,0 +1,33 @@ +import localforage from 'localforage'; + +import * as logging from '../logging'; +import { P2pClient } from '../p2p-client'; +import status from '../status'; + +declare const self: { + SamizdAppDevTools: SamizdAppDevTools; +} & ServiceWorkerGlobalScope; + +export class SamizdAppDevTools { + public logging = logging; + public status = status; + public localforage = localforage; + + constructor(public p2pClient: P2pClient) { + // attach to window + self.SamizdAppDevTools = this; + } + + public get addressBook() { + return this.p2pClient.node?.peerStore + .all() + .then(peers => + Object.fromEntries( + peers.map(it => [ + it.id.toString(), + it.addresses.map(it => it.multiaddr.toString()), + ]) + ) + ); + } +} diff --git a/packages/gateway-client/src/worker/fetch-handlers/fetch-handlers.spec.ts b/packages/gateway-client/src/worker/fetch-handlers/fetch-handlers.spec.ts new file mode 100644 index 0000000..12553b2 --- /dev/null +++ b/packages/gateway-client/src/worker/fetch-handlers/fetch-handlers.spec.ts @@ -0,0 +1,11 @@ +import fetchHandlers from './fetch-handlers'; + +describe('fetchHandlers', () => { + it('.use() should add a handler', () => { + const handler = () => { + /*empty*/ + }; + fetchHandlers.use(handler); + expect(fetchHandlers['handlers']).toContain(handler); + }); +}); diff --git a/packages/gateway-client/src/worker/fetch-handlers/fetch-handlers.ts b/packages/gateway-client/src/worker/fetch-handlers/fetch-handlers.ts new file mode 100644 index 0000000..6cc0bc5 --- /dev/null +++ b/packages/gateway-client/src/worker/fetch-handlers/fetch-handlers.ts @@ -0,0 +1,44 @@ +import { logger } from '../logging'; + +declare const self: ServiceWorkerGlobalScope; + +const log = logger.getLogger('worker/fetch/handlers'); + +export type Handler = ( + request: Request, + respondWith: FetchEvent['respondWith'] +) => void; + +class FetchHandlers { + private handlers: Handler[] = []; + + constructor() { + self.addEventListener('fetch', event => { + log.trace('Received fetch: ', event); + + // define custom respondWith method that tracks if it has been + // called or not (so we know when to stop) + let responded = false; + const respondWith = ( + response: Parameters[0] + ) => { + responded = true; + event.respondWith(response); + }; + // loop our handlers until respondWith is called + for (const handler of this.handlers) { + handler(event.request, respondWith); + if (responded) { + break; + } + } + }); + } + + use(handler: Handler) { + this.handlers.push(handler); + return this; + } +} + +export default new FetchHandlers(); diff --git a/packages/gateway-client/src/worker/fetch-handlers/index.ts b/packages/gateway-client/src/worker/fetch-handlers/index.ts new file mode 100644 index 0000000..141e1ec --- /dev/null +++ b/packages/gateway-client/src/worker/fetch-handlers/index.ts @@ -0,0 +1,4 @@ +export * from './fetch-handlers'; +export * from './pass-through-handler'; +export * from './pleroma-timeline-handler'; +export * from './static-cache-handler'; diff --git a/packages/gateway-client/src/worker/fetch-handlers/pass-through-handler.spec.ts b/packages/gateway-client/src/worker/fetch-handlers/pass-through-handler.spec.ts new file mode 100644 index 0000000..1bc00db --- /dev/null +++ b/packages/gateway-client/src/worker/fetch-handlers/pass-through-handler.spec.ts @@ -0,0 +1,18 @@ +import { passThroughHandler } from './pass-through-handler'; + +describe('passThroughHandler() should', () => { + beforeEach(() => { + global.fetch = jest.fn(); + }); + + afterEach(() => { + (global.fetch as jest.Mock).mockClear(); + }); + + it('Pass through the request', async () => { + const request = { url: 'https://example.com' }; + const respondWith = jest.fn(); + passThroughHandler(request as Request, respondWith); + expect(respondWith).toHaveBeenCalledWith(fetch(request as Request)); + }); +}); diff --git a/packages/gateway-client/src/worker/fetch-handlers/pass-through-handler.ts b/packages/gateway-client/src/worker/fetch-handlers/pass-through-handler.ts new file mode 100644 index 0000000..3bc9f88 --- /dev/null +++ b/packages/gateway-client/src/worker/fetch-handlers/pass-through-handler.ts @@ -0,0 +1,7 @@ +import { Handler } from './fetch-handlers'; + +export const passThroughHandler: Handler = (request, respondWith) => { + respondWith(fetch(request)); +}; + +export default passThroughHandler; diff --git a/packages/gateway-client/src/worker/fetch-handlers/pleroma-timeline-handler.spec.ts b/packages/gateway-client/src/worker/fetch-handlers/pleroma-timeline-handler.spec.ts new file mode 100644 index 0000000..53674ac --- /dev/null +++ b/packages/gateway-client/src/worker/fetch-handlers/pleroma-timeline-handler.spec.ts @@ -0,0 +1,33 @@ +// test pleromaTimelineHandler +import { getWindowClient } from '../client'; +import { pleromaTimelineHandler } from './pleroma-timeline-handler'; + +let mockClient = {} as WindowClient; + +jest.mock('../client', () => ({ + getWindowClient: jest.fn(() => Promise.resolve(mockClient)), +})); + +describe('pleromaTimelineHandler() should', () => { + beforeEach(() => { + global.fetch = jest.fn(); + mockClient = { + navigate: jest.fn(), + } as unknown as WindowClient; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('Navigate to /timeline/fediverse if on /timeline/local', async () => { + const request = { + url: 'https://example.com/api/v1/timelines/public?local=true', + }; + const respondWith = jest.fn(); + pleromaTimelineHandler(request as Request, respondWith); + expect(respondWith).toHaveBeenCalledWith(fetch(request as Request)); + await getWindowClient(() => true); + expect(mockClient.navigate).toHaveBeenCalledWith('/timeline/fediverse'); + }); +}); diff --git a/packages/gateway-client/src/worker/fetch-handlers/pleroma-timeline-handler.ts b/packages/gateway-client/src/worker/fetch-handlers/pleroma-timeline-handler.ts new file mode 100644 index 0000000..2300a0c --- /dev/null +++ b/packages/gateway-client/src/worker/fetch-handlers/pleroma-timeline-handler.ts @@ -0,0 +1,21 @@ +import { getWindowClient } from '../client'; +import { Handler } from './fetch-handlers'; + +export const pleromaTimelineHandler: Handler = (request, respondWith) => { + // handle requests to the pleroma timeline + const { pathname, searchParams } = new URL(request.url); + if (pathname === '/api/v1/timelines/public' && searchParams.get('local')) { + getWindowClient( + it => new URL(it.url).pathname === '/timeline/local' + ).then(atPleromaPage => { + if (atPleromaPage) { + atPleromaPage.navigate('/timeline/fediverse'); + } + }); + } + + // always pass the request to fetch regardless + respondWith(fetch(request)); +}; + +export default pleromaTimelineHandler; diff --git a/packages/gateway-client/src/worker/fetch-handlers/static-cache-handler.spec.ts b/packages/gateway-client/src/worker/fetch-handlers/static-cache-handler.spec.ts new file mode 100644 index 0000000..d2e35d7 --- /dev/null +++ b/packages/gateway-client/src/worker/fetch-handlers/static-cache-handler.spec.ts @@ -0,0 +1,29 @@ +import { staticCacheHandler } from './static-cache-handler'; + +describe('staticCacheHandler() should', () => { + beforeEach(() => { + global.fetch = jest.fn(() => Promise.resolve({} as Response)); + global.caches = { + open: jest.fn(() => + Promise.resolve({ + match: jest.fn(() => Promise.resolve({} as Response)), + put: jest.fn(), + } as unknown as Cache) + ), + } as unknown as CacheStorage; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('Cache static assets', async () => { + const request = { + url: 'https://example.com/assets/example.png', + destination: 'image', + }; + const respondWith = jest.fn(); + staticCacheHandler(request as Request, respondWith); + expect(respondWith).toHaveBeenCalledWith(fetch(request as Request)); + }); +}); diff --git a/packages/gateway-client/src/worker/fetch-handlers/static-cache-handler.ts b/packages/gateway-client/src/worker/fetch-handlers/static-cache-handler.ts new file mode 100644 index 0000000..e40572b --- /dev/null +++ b/packages/gateway-client/src/worker/fetch-handlers/static-cache-handler.ts @@ -0,0 +1,60 @@ +import { logger } from '../logging'; +import { Handler } from './fetch-handlers'; + +const log = logger.getLogger('worker/fetch/static-cache-handler'); + +export const staticCacheHandler: Handler = (request, respondWith) => { + // Check if this is a request for a static asset + log.trace('Destination: ', request.destination, request); + + // only handle static assets + if ( + ![ + 'audio', + 'audioworklet', + 'document', + 'font', + 'image', + 'paintworklet', + 'report', + 'script', + 'style', + 'track', + 'video', + 'xslt', + ].includes(request.destination) && + // this directory doesn't have a usable destination string, but it's static assets + !request.url.includes('/packs/icons/') + ) { + return; + } + + // don't cache when running locally + if (process.env.NX_LOCAL === 'true') { + return; + } + + // respond with asset from either cache or fetch + respondWith( + caches.open('pwa-static-cache').then(cache => { + // Go to the cache first + return cache.match(request.url).then(cachedResponse => { + // Return a cached response if we have one + if (cachedResponse) { + return cachedResponse; + } + + // Otherwise, hit the network + return fetch(request).then(fetchedResponse => { + // Add the network response to the cache for later visits + cache.put(request, fetchedResponse.clone()); + + // Return the network response + return fetchedResponse; + }); + }); + }) + ); +}; + +export default staticCacheHandler; diff --git a/packages/gateway-client/src/worker/index.ts b/packages/gateway-client/src/worker/index.ts deleted file mode 100644 index 6a89912..0000000 --- a/packages/gateway-client/src/worker/index.ts +++ /dev/null @@ -1,1150 +0,0 @@ -import { Noise } from '@chainsafe/libp2p-noise'; -import { Bootstrap } from '@libp2p/bootstrap'; -import { Stream } from '@libp2p/interface-connection'; -import { ConnectionEncrypter } from '@libp2p/interface-connection-encrypter'; -import { PeerId } from '@libp2p/interface-peer-id'; -import type { Address } from '@libp2p/interface-peer-store'; -import { StreamMuxerFactory } from '@libp2p/interface-stream-muxer'; -import * as libp2pLogger from '@libp2p/logger'; -import { Mplex } from '@libp2p/mplex'; -import { isLoopback } from '@libp2p/utils/multiaddr/is-loopback'; -import { isPrivate } from '@libp2p/utils/multiaddr/is-private'; -import { WebSockets } from '@libp2p/websockets'; -import { all as WSAllfilter } from '@libp2p/websockets/filters'; -import { Multiaddr as MultiaddrType } from '@multiformats/multiaddr'; -import { Buffer } from 'buffer/'; -import { pipe } from 'it-pipe'; -import { createLibp2p, Libp2p } from 'libp2p'; -import { decode, encode } from 'lob-enc'; -import localforage from 'localforage'; -import Multiaddr from 'multiaddr'; -import * as workboxPrecaching from 'workbox-precaching'; -import { KEEP_ALIVE } from '@libp2p/interface-peer-store/tags'; -import type * as it from 'it-stream-types'; -import { - WorkerMessageType, - ServerPeerStatus, - Message, - ClientMessageType, -} from '../service-worker'; - -// the workbox-precaching import includes a type definition for -// . -// Import it even though we're not using any of the imports, -// and mark the import as being used with this line: -const _ = workboxPrecaching; - -// type Window = { -// localStorage: { -// debug: string; -// } -// } - -// type Document = {} - -enum LogLevel { - OFF = 'OFF', - ERROR = 'ERROR', - WARN = 'WARN', - INFO = 'INFO', - DEBUG = 'DEBUG', - TRACE = 'TRACE', -} - -declare const self: { - status: WorkerStatus; - soapstore: LocalForage; - DIAL_TIMEOUT: number; - serverPeer: PeerId; - node: Libp2p; - libp2p: Libp2p; - deferral: Promise; - stashedFetch: typeof fetch; - Buffer: typeof Buffer; - Multiaddr: typeof Multiaddr; - _fetch: typeof fetch; - getStream: typeof getStream; - localforage: typeof localforage; - streamFactory: AsyncGenerator; - // window: Window; - // document: Document; - libp2pSetLogLevel: (level: LogLevel) => void; - latencyMap: Map; - latencySet: Set; - pipersInProgress: Map>; - process: { - version: string; - }; -} & ServiceWorkerGlobalScope; -self.process = { - version: self.navigator.userAgent, -}; -self.latencyMap = new Map(); -self.latencySet = new Set(); -self.pipersInProgress = new Map(); - -self.libp2pSetLogLevel = (level: LogLevel) => { - const levelHandlers: Record void> = { - OFF: () => libp2pLogger.disable(), - ERROR: extra => - libp2pLogger.enable( - 'libp2p:circuit:error, libp2p:bootstrap:error, libp2p:upgrader:error, ' + - extra - ), - WARN: extra => levelHandlers.ERROR('libp2p:websockets:error, ' + extra), - INFO: extra => - levelHandlers.WARN( - 'libp2p:dialer:error, libp2p:connection-manager:trace, ' + extra - ), - DEBUG: extra => - levelHandlers.INFO( - 'libp2p:peer-store:trace, libp2p:mplex:stream:trace, libp2p:*:error, ' + - extra - ), - TRACE: extra => levelHandlers.DEBUG('libp2p:*:trace, ' + extra), - }; - - levelHandlers[level](); -}; - -self.libp2pSetLogLevel(LogLevel.ERROR); - -// slightly modified version of -// https://github.com/libp2p/js-libp2p-utils/blob/66e604cb0bfcf686eb68e44f278d62e3464c827c/src/address-sort.ts -// the goal here is to couple prioritizing relays with parallelism -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -//@ts-ignore -self.addrSort = publicRelayAddressesFirst; -function publicRelayAddressesFirst(a: Address, b: Address): -1 | 0 | 1 { - // console.log('spam sort', a.multiaddr.toString(), b.multiaddr.toString()); - const haveLatencyA = self.latencyMap.has(a.multiaddr.toString()); - const haveLatencyB = self.latencyMap.has(b.multiaddr.toString()); - - // if we only have one latency, it's the one we want - if (haveLatencyA && !haveLatencyB) { - return -1; - } - if (!haveLatencyA && haveLatencyB) { - return 1; - } - - if (haveLatencyA && haveLatencyB) { - // if we have latency info for both, prefer non relay - const isARelay = isRelay(a); - const isBRelay = isRelay(b); - if (isARelay && !isBRelay) { - return 1; - } - if (!isARelay && isBRelay) { - return -1; - } - // if both/neither are relays, prefer the one with lower latency - const latencyA = - self.latencyMap.get(a.multiaddr.toString()) || Infinity; - const latencyB = - self.latencyMap.get(b.multiaddr.toString()) || Infinity; - if (latencyA < latencyB) { - return -1; - } - if (latencyA > latencyB) { - return 1; - } - - // if both have the same latency, return 0 - return 0; - } - - // we should never get here, but not sure on where this vs filter - // is called, so leaving old logic just in case; - - const isADNS = isDNS(a); - const isBDNS = isDNS(b); - const isAPrivate = isPrivate(a.multiaddr); - const isBPrivate = isPrivate(b.multiaddr); - - if (isADNS && !isBDNS) { - return 1; - } else if (!isADNS && isBDNS) { - return -1; - } else if (isAPrivate && !isBPrivate) { - return 1; - } else if (!isAPrivate && isBPrivate) { - return -1; - } else if (!(isAPrivate || isBPrivate)) { - const isARelay = isRelay(a); - const isBRelay = isRelay(b); - - if (isARelay && !isBRelay) { - return -1; - } else if (!isARelay && isBRelay) { - return 1; - } else { - return 0; - } - } else if (isAPrivate && isBPrivate) { - const isALoopback = isLoopback(a.multiaddr); - const isBLoopback = isLoopback(b.multiaddr); - - if (isALoopback && !isBLoopback) { - return 1; - } else if (!isALoopback && isBLoopback) { - return -1; - } else { - return 0; - } - } - - return 0; -} - -function isRelay(ma: Address): boolean { - const parts = new Set(ma.multiaddr.toString().split('/')); - return parts.has('p2p-circuit'); -} -function isDNS(ma: Address): boolean { - const parts = new Set(ma.multiaddr.toString().split('/')); - return parts.has('dns4'); -} -self.localforage = localforage; - -async function getWSOpenLatency(ma: string): Promise { - return new Promise(resolve => { - setTimeout(resolve, 5000, Infinity); - try { - const [_nil, _type, host, _tcp, port, _ws, _p2p, id] = - ma.split('/'); - const start = Date.now(); - const ws = new WebSocket(`ws://${host}:${port}/p2p/${id}`); - ws.onopen = () => { - ws.close(); - resolve(Date.now() - start); - }; - ws.onerror = () => resolve(Infinity); - } catch (e) { - console.error(e); - resolve(Infinity); - } - }); -} - -async function checkAddress(address: string): Promise { - const latency = await getWSOpenLatency(address); - // console.log('spam latency', address, latency); - if (latency < Infinity) { - self.latencyMap.set(address, latency); - self.latencySet.add(address.split('/p2p-circuit')[0]); - return true; - } - - return false; -} - -async function initCheckAddresses(addresses: string[]): Promise { - self.latencyMap = new Map(); - self.latencySet = new Set(); - await Promise.all(addresses.map(checkAddress)); - return addresses.filter(a => self.latencyMap.has(a)); -} - -const WB_MANIFEST = self.__WB_MANIFEST; -// const wbManifestUrls = WB_MANIFEST.map(it => -// (it as PrecacheEntry).revision ? (it as PrecacheEntry).url : it -// ); - -// self.window = { localStorage: { debug: '' } } -// self.document = {} - -// Precache all of the assets generated by your build process. -// Their URLs are injected into the manifest variable below. -// This variable must be present somewhere in your service worker file, -// even if you decide not to use precaching. See https://cra.link/PWA -//precacheAndRoute(WB_MANIFEST); -console.log(WB_MANIFEST); - -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -//@ts-ignore -self.setImmediate = (fn: () => void) => self.setTimeout(fn, 0); - -// To disable all workbox logging during development, you can set self.__WB_DISABLE_DEV_LOGS to true -// https://developers.google.com/web/tools/workbox/guides/configure-workbox#disable_logging -// -// self.__WB_DISABLE_DEV_LOGS = true - -self.DIAL_TIMEOUT = 3000; - -self.Multiaddr = Multiaddr; - -const CHUNK_SIZE = 1024 * 64; -self.Buffer = Buffer; - -self._fetch = fetch; - -const waitFor = async (t: number): Promise => - new Promise(r => setTimeout(r, t)); - -type StreamMaker = (protocol: string, id: string) => Promise; - -async function* streamFactoryGenerator(): AsyncGenerator< - StreamMaker, - void, - string | undefined -> { - let locked = false; - let dialTimeout = self.DIAL_TIMEOUT; - let retryTimeout = 0; - - const makeStream: StreamMaker = async function (protocol, _piperId) { - // console.log('get protocol stream', protocol) - let streamOrNull = null; - while (!streamOrNull) { - while (locked) { - console.log('waiting for reset lock'); - await waitFor(100); - } - // attempt to dial our peer, track the time it takes - const start = Date.now(); - streamOrNull = await Promise.race([ - self.node.dialProtocol(self.serverPeer, protocol).catch(_ => { - // console.log('dialProtocol error, retry', Date.now() - start); - // console.log('dialProtocol error, retry', _); - return null; - }), - waitFor(dialTimeout), - ]); - - // if we successfully, dialed, we have a stream - if (streamOrNull) { - // reset our timeouts - // use the time of the dial to calculate an - // appropriate dial timeout - dialTimeout = Math.max( - self.DIAL_TIMEOUT, - Math.floor((Date.now() - start) * 4) - ); - retryTimeout = 0; - // if we were NOT previously connected - if (self.status.serverPeer !== ServerPeerStatus.CONNECTED) { - // we are now - self.status.serverPeer = ServerPeerStatus.CONNECTED; - } - // we have a stream, we can quit now - // console.log('got stream', protocol, streamOrNull) - break; - } else if (!locked) { - locked = true; - - // this is a connection error, if we were previously connected - if (self.status.serverPeer === ServerPeerStatus.CONNECTED) { - // we aren't anymore - self.status.serverPeer = ServerPeerStatus.CONNECTING; - } - - // if our retry timeout reaches 5 seconds, then we'll have - // been retrying for 15 seconds (triangle number of 5). - // By this point, we're probably offline. - if ( - self.status.serverPeer !== ServerPeerStatus.OFFLINE && - retryTimeout >= 5000 - ) { - self.status.serverPeer = ServerPeerStatus.OFFLINE; - } - - // wait before retrying - console.log('Dial timeout, waiting to reset...', { - dialTimeout, - retryTimeout, - }); - - await waitFor(retryTimeout); - - // for (const [id, piper] of self.pipersInProgress) { - // if (id !== piperId) { - // await piper; - // } - // } - - // if our retry timeout reaches 30 seconds, then we'll have - // been retrying for 5 minutes 45 seconds - // (triangle number of 30) - // time to reset - if (retryTimeout >= 30000) { - retryTimeout = 0; - } - // increase our retry timeout - retryTimeout += 1000; - // increase our dial timeout, but never make it higher than - // 5 minutes - dialTimeout = Math.min(1000 * 60 * 5, dialTimeout * 4); - - // now that we've waiting, we can retry - // locked = true; - console.log('Resetting libp2p...'); - const _s = Date.now(); - let bootstraplist = await getBootstrapList(true); - if (retryTimeout >= 2000) { - bootstraplist = await initCheckAddresses(bootstraplist); - } - - await self.node.stop(); - await self.node.start(); - const relays = bootstraplist.map( - s => Multiaddr.multiaddr(s) as unknown as MultiaddrType - ); - await self.node.peerStore.addressBook - .add(self.serverPeer, relays) - .catch(_ => _); - console.log('reset time', Date.now() - _s); - locked = false; - } - } - return streamOrNull; - }; - - while (true) { - // locked = true; - yield makeStream; - } -} -class WorkerStatus { - _serverPeer: ServerPeerStatus | null = null; - relays: string[] = []; - - constructor() { - Reflect.defineProperty(this.relays, 'push', { - value: (...items: string[]): number => { - const ret = Array.prototype.push.call(this.relays, ...items); - getClient().then(client => { - client?.postMessage({ - type: WorkerMessageType.LOADED_RELAYS, - relays: this.relays, - }); - }); - return ret; - }, - }); - } - - get serverPeer() { - return this._serverPeer; - } - - set serverPeer(status: ServerPeerStatus | null) { - this._serverPeer = status; - getClient().then(client => { - client?.postMessage({ - type: WorkerMessageType.SERVER_PEER_STATUS, - status, - }); - }); - } - - async sendCurrent() { - const client = await getClient(); - client?.postMessage({ - type: WorkerMessageType.LOADED_RELAYS, - relays: this.relays, - }); - client?.postMessage({ - type: WorkerMessageType.SERVER_PEER_STATUS, - status: this.serverPeer, - }); - } -} -self.status = new WorkerStatus(); - -const isBootstrapAppUrl = (url: URL): boolean => - url.pathname.startsWith('/smz/pwa'); - -const getClient = async (): Promise => { - const allClients = await self.clients.matchAll(); - return allClients.find( - it => it instanceof WindowClient && isBootstrapAppUrl(new URL(it.url)) - ) as WindowClient; -}; - -async function normalizeBody(body: unknown) { - try { - if (!body) return undefined; - if (typeof body === 'string') return Buffer.from(body); - if (Buffer.isBuffer(body)) return body; - if (body instanceof ArrayBuffer) { - if (body.byteLength > 0) return Buffer.from(new Uint8Array(body)); - return undefined; - } - type WithArrayBuffer = { arrayBuffer: () => Promise }; - if ((body as WithArrayBuffer).arrayBuffer) { - return Buffer.from( - new Uint8Array(await (body as WithArrayBuffer).arrayBuffer()) - ); - } - if ((body as ReadableStream).toString() === '[object ReadableStream]') { - const reader = (body as ReadableStream).getReader(); - const chunks = []; - let _done = false; - do { - const { done, value } = await reader.read(); - _done = done; - chunks.push(Buffer.from(new Uint8Array(value))); - } while (!_done); - return Buffer.concat(chunks); - } - - throw new Error(`don't know how to handle body`); - } catch (e) { - return Buffer.from( - `${(e as Error).message} ${typeof body} ${( - body as string - ).toString()} ${JSON.stringify(body)}` - ); - } -} - -async function getStream( - protocol = '/samizdapp-proxy', - id: string = crypto.randomUUID() -) { - const { value } = await self.streamFactory.next(); - - const makeStream = value as StreamMaker; - return makeStream(protocol, id); -} - -self.getStream = getStream; - -async function p2Fetch( - givenReqObj: URL | RequestInfo, - givenReqInit: RequestInit | undefined = {}, - _xhr?: XMLHttpRequest -): Promise { - // assert that we were given a request - givenReqObj = givenReqObj as Request; - - if (typeof givenReqObj.url != 'string') { - throw new Error( - `Patched service worker \`fetch()\` method expects a full request object, received ${givenReqObj.constructor.name}` - ); - } - - // patch args - const body = - givenReqObj.body ?? - givenReqInit.body ?? - (await givenReqObj.arrayBuffer?.()) ?? - null; - const { reqObj, reqInit } = patchFetchArgs(givenReqObj, givenReqInit); - - // apply filtering to the request - const url = new URL( - reqObj.url.startsWith('http') - ? reqObj.url - : `http://localhost${reqObj.url}` - ); - - if (process.env.NX_LOCAL === 'true' && isBootstrapAppUrl(url)) { - return self.stashedFetch(givenReqObj, givenReqInit); - } - - if (url.pathname.startsWith('/smz')) { - reqObj.headers['X-Intercepted-Subdomain'] = 'samizdapp'; - } else if (url.pathname !== '/manifest.json') { - reqObj.headers['X-Intercepted-Subdomain'] = 'pleroma'; - } - - if (url.host === getHost()) { - url.host = 'localhost'; - url.protocol = 'http:'; - url.port = '80'; - } - - reqObj.url = url.toString(); - - // console.log("pocketFetch2", reqObj, reqInit, body); - //delete (reqObj as Request).body; - delete reqInit?.body; - const pbody = await normalizeBody(body); - const packet = encode({ reqObj, reqInit }, pbody); - // console.log('packet:', packet.toString('hex')) - let i = 0; - const parts: Buffer[] = []; - for (; i <= Math.floor(packet.length / CHUNK_SIZE); i++) { - parts.push( - packet.slice( - i * CHUNK_SIZE, - (i + 1) * CHUNK_SIZE - ) as unknown as Buffer - ); - } - - parts.push(Buffer.from([0x00])); - // console.log('packet?', packet) - // console.log('get fetch stream'); - - // console.log('parts:') - // parts.forEach(p => console.log(p.toString('hex'))) - // let j = 0; - let done = false; - let res_parts: Buffer[] = []; - let floatMax = parts.length * 5000; - - async function piper(piperId: string) { - const st = Date.now(); - const stream = await getStream('/samizdapp-proxy', piperId); - console.log('time to stream', Date.now() - st); - - let float = 0, - t = Date.now(), - gotFirstChunk = false; - // console.log('piper parts', parts); - pipe( - parts, - stream as unknown as it.Duplex, - async function gatherResponse(source) { - for await (const msg of source) { - if (gotFirstChunk) { - float = Math.max(float, Date.now() - t); - } else { - console.log('time to first chunk', Date.now() - t); - gotFirstChunk = true; - } - t = Date.now(); - const buf = Buffer.from(msg.subarray()); - if (msg.subarray().length === 1 && buf[0] === 0x00) { - done = true; - } else { - res_parts.push(buf); - } - } - } - ); - - while (!done) { - await waitFor(100); - if (float && Date.now() - t > Math.max(500, float * 4)) { - console.log('time float', float); - break; - } - if (!float && Date.now() - t > floatMax) { - console.log('time floatMax', floatMax); - floatMax += parts.length * 5000; - break; - } - if (self.status.serverPeer !== ServerPeerStatus.CONNECTED) { - console.log('time serverPeer', self.status.serverPeer); - break; - } - } - - stream.close(); - // console.log('piper finish'); - } - - let j = 0; - while (!done) { - console.log('try', j++, reqObj.url); - res_parts = []; - const piperId = crypto.randomUUID(); - const piperProm = piper(piperId); - self.pipersInProgress.set(piperId, piperProm); - await piperProm; - self.pipersInProgress.delete(piperId); - // await piper(); - console.log('time done', done); - } - - const resp = decode(Buffer.concat(res_parts)); - if (!resp.json.res) { - throw resp.json.error; - } - resp.json.res.headers = new Headers(resp.json.res.headers); - // alert("complete"); - return new Response(resp.body, resp.json.res); -} - -const getHost = () => { - try { - return window.location.host; - } catch (e) { - return self.location.host; - } -}; - -function patchFetchArgs(_reqObj: Request, _reqInit: RequestInit = {}) { - // console.log("patch"); - - const rawHeaders = Object.fromEntries(_reqObj.headers.entries()); - - const reqObj = { - bodyUsed: _reqObj.bodyUsed, - cache: _reqObj.cache, - credentials: _reqObj.credentials, - destination: _reqObj.destination, - headers: rawHeaders, - integrity: _reqObj.integrity, - isHistoryNavigation: ( - _reqObj as Request & { isHistoryNavigation: boolean } - ).isHistoryNavigation, - keepalive: _reqObj.keepalive, - method: _reqObj.method, - mode: _reqObj.mode, - redirect: _reqObj.redirect, - referrer: _reqObj.referrer, - referrerPolicy: _reqObj.referrerPolicy, - url: _reqObj.url, - }; - - const reqInit = { - ..._reqInit, - headers: rawHeaders, - }; - - return { reqObj, reqInit }; -} - -async function openRelayStream(cb: () => unknown) { - const stream = await getStream('/samizdapp-relay'); - let gotFirstRelay = false; - // console.log('got relay stream'); - await pipe(stream.source, async function (source) { - for await (const msg of source) { - const str_relay = Buffer.from(msg.subarray()).toString(); - if (await checkAddress(str_relay)) { - if (!gotFirstRelay) { - gotFirstRelay = true; - cb(); - await localforage.setItem('libp2p.relays', []); - } - await localforage - .getItem('libp2p.relays') - .then(str_array => { - const dedup = Array.from( - new Set([str_relay, ...(str_array || [])]) - ); - - return localforage.setItem('libp2p.relays', dedup); - }); - const multiaddr = Multiaddr.multiaddr( - str_relay - ) as unknown as MultiaddrType; - - await self.node.peerStore.addressBook - .add(self.serverPeer, [multiaddr]) - .catch(_ => _); - - // update status - if (!self.status.relays.includes(str_relay)) { - self.status.relays.push(str_relay); - } - } - } - }).catch(e => { - console.log('error in pipe', e); - }); - // we wan't fetch streams to have priority, so let's ease up this loop - await new Promise(r => setTimeout(r, 20000)); -} - -function getHostAddrs(hostname: string, tail: string[]): string[] { - const res = [`/dns4/${hostname}/${tail.join('/')}`]; - if (hostname.endsWith('localhost')) { - res.push( - `/dns4/${hostname.substring(0, hostname.length - 4)}/${tail.join( - '/' - )}` - ); - } - console.log('getHostAddrs', res); - return res; -} - -async function getBootstrapList(skipFetch = false) { - let newBootstrapAddress = null; - try { - if (!skipFetch) { - newBootstrapAddress = await self - .stashedFetch('/smz/pwa/assets/libp2p.bootstrap') - .then(res => { - if (res.status >= 400) { - throw res; - } - return res.text(); - }) - .then(text => text.trim()); - } - } catch (e) { - console.debug('Error while trying to fetch new bootstrap address: ', e); - } - const cachedBootstrapAddress = - (await localforage.getItem('libp2p.bootstrap')) ?? null; - const bootstrapaddr = newBootstrapAddress || cachedBootstrapAddress; - if (bootstrapaddr !== cachedBootstrapAddress) { - console.debug( - 'Detected updated bootstrap address, updating cache: ', - bootstrapaddr - ); - await localforage.setItem('libp2p.bootstrap', bootstrapaddr); - } - - console.debug('got bootstrap addr', bootstrapaddr); - const relay_addrs = - (await localforage.getItem('libp2p.relays').catch(_ => [])) ?? - []; - console.debug('got relay addrs', relay_addrs); - - const { hostname } = new URL(self.origin); - const [_, _proto, _ip, ...rest] = bootstrapaddr?.split('/') ?? []; - const hostaddrs = getHostAddrs(hostname, rest); - const res = [bootstrapaddr ?? '', ...hostaddrs, ...relay_addrs].filter( - notEmpty => notEmpty - ); - return res; -} - -function websocketAddressFilter(addresses: MultiaddrType[]) { - const res = WSAllfilter(addresses).filter((addr: MultiaddrType) => { - // console.log('filter?', addr.toString(), selt.lat); - return self.latencySet.has(addr.toString()); - }); - // console.log('ran filter', res); - return res; -} - -const getQuickestPath = (): string | null => { - let quickest = Infinity; - let quickestAddr = null; - for (const [addr, latency] of self.latencyMap.entries()) { - if (latency < quickest) { - quickest = latency; - quickestAddr = addr; - } - } - return quickestAddr; -}; - -async function main() { - // self.window.localStorage.debug = (await localforage.getItem('debug')) || "" - - const bootstraplist = await initCheckAddresses(await getBootstrapList()); - console.log('bootstraplist', bootstraplist); - // await initCheckAddresses(bootstraplist); - // update status - self.status.serverPeer = ServerPeerStatus.BOOTSTRAPPED; - - self.status.relays.push(...bootstraplist.slice(2)); - - // const datastore = new LevelDatastore('./libp2p'); - // await datastore.open(); // level database must be ready before node boot - const serverID = bootstraplist[0].split('/').pop(); - const node = await createLibp2p({ - // datastore, - transports: [ - new WebSockets({ - filter: websocketAddressFilter, - }), - ], - connectionEncryption: [new Noise() as unknown as ConnectionEncrypter], - streamMuxers: [new Mplex() as StreamMuxerFactory], - peerDiscovery: [ - new Bootstrap({ - list: bootstraplist, // provide array of multiaddrs - }), - ], - connectionManager: { - autoDial: true, // Auto connect to discovered peers (limited by ConnectionManager minConnections) - minConnections: 3, - maxDialsPerPeer: 20, - maxParallelDials: 20, - addressSorter: publicRelayAddressesFirst, - // The `tag` property will be searched when creating the instance of your Peer Discovery service. - // The associated object, will be passed to the service when it is instantiated. - // dialTimeout: self.DIAL_TIMEOUT, - // maxParallelDials: 25, - // maxAddrsToDial: 25, - // resolvers: { - // dnsaddr: dnsaddrResolver, - // // , - // // host: hostResolver - // }, - }, - relay: { - // Circuit Relay options (this config is part of libp2p core configurations) - enabled: true, // Allows you to dial and accept relayed connections. Does not make you a relay. - autoRelay: { - enabled: true, // Allows you to bind to relays with HOP enabled for improving node dialability - maxListeners: 5, // Configure maximum number of HOP relays to use - }, - }, - }); - - node.addEventListener('peer:discovery', evt => { - const peer = evt.detail; - console.log(`Found peer ${peer.id.toString()}`); - }); - - // Listen for new connections to peers - let relayDebounce = 0; - const connectPromise = new Promise((resolve, reject) => { - try { - node.connectionManager.addEventListener( - 'peer:connect', - async evt => { - const connection = evt.detail; - const str_id = connection.remotePeer.toString(); - await node.peerStore - .tagPeer(connection.remotePeer, KEEP_ALIVE) - .catch(_ => null); - if (str_id === serverID) { - // update status - console.log('connected to server'); - - self.serverPeer = connection.remotePeer; - self.status.serverPeer = ServerPeerStatus.CONNECTED; - - if (Date.now() - relayDebounce > 60000) { - relayDebounce = Date.now(); - openRelayStream(() => { - /* - * Don't wait for a relay before resolving. - * - * A relay is required in order to access the box - * outside of the box's LAN. There are currently - * two methods of obtaining a relay: a UPnP address - * on the local network and a public UPnP address - * on the SamizdApp network; however, both methods - * are currently unreliable. - * - * Until we have a way of reliably obtaining a - * public relay, do not wait for a public relay - * before resolving. - * - * TODO: Strengthen one of the methods for - * obtaining a public relay address. - * - */ - //resolve(); - }); - } - - // TODO: Don't resolve here once we have a reliable way - // of obtaining a public relay - resolve(); - } - } - ); - } catch (e) { - reject(e); - } - }); - // Listen for peers disconnecting - node.connectionManager.addEventListener('peer:disconnect', evt => { - const connection = evt.detail; - console.log(`Disconnected from ${connection.remotePeer.toString()}`); - if (connection.remotePeer.equals(self.serverPeer)) { - console.log('disconnected from server'); - // update status - self.status.serverPeer = ServerPeerStatus.CONNECTING; - node.dial(self.serverPeer); - } - }); - console.debug('starting libp2p'); - - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - //@ts-ignore - // node.components.setPeerStore(new PersistentPeerStore()); - self.streamFactory = streamFactoryGenerator(); - // await self.streamFactory.next(); - await node.start(); - console.debug('started libp2p'); - - const path = getQuickestPath(); - if (path) { - node.dial(path as unknown as MultiaddrType); - } - - // update status - self.status.serverPeer = ServerPeerStatus.CONNECTING; - - Promise.race([connectPromise, waitFor(15000)]).then(() => { - if (self.status.serverPeer === ServerPeerStatus.CONNECTING) { - self.status.serverPeer = ServerPeerStatus.OFFLINE; - } - }); - - self.libp2p = self.node = node; - return connectPromise; -} - -const maybeNavigateToFediverse = async (event: FetchEvent) => { - const { pathname, searchParams } = new URL(event.request.url); - if (pathname === '/api/v1/timelines/public' && searchParams.get('local')) { - const allClients = await self.clients.matchAll(); - const atPleromaPage = allClients.find( - it => - it instanceof WindowClient && - new URL(it.url).pathname === '/timeline/local' - ) as WindowClient; - - if (atPleromaPage) { - atPleromaPage.navigate('/timeline/fediverse'); - } - } -}; - -self.addEventListener('fetch', function (event) { - // console.log('Received fetch: ', event); - - // Check if this is a request for a static asset - // console.log('destination', event.request.destination, event.request) - if ( - [ - 'audio', - 'audioworklet', - 'document', - 'font', - 'image', - 'paintworklet', - 'report', - 'script', - 'style', - 'track', - 'video', - 'xslt', - ].includes(event.request.destination) || - // this directory doesn't have a usable destination string, but it's static assets - event.request.url.includes('/packs/icons/') - ) { - event.respondWith( - caches.open('pwa-static-cache').then(cache => { - // Go to the cache first - return cache.match(event.request.url).then(cachedResponse => { - // Return a cached response if we have one - if (cachedResponse) { - return cachedResponse; - } - - // Otherwise, hit the network - return fetch(event.request).then(fetchedResponse => { - // Add the network response to the cache for later visits - cache.put(event.request, fetchedResponse.clone()); - - // Return the network response - return fetchedResponse; - }); - }); - }) - ); - } else { - maybeNavigateToFediverse(event); - event?.respondWith(fetch(event.request)); - } -}); - -self.addEventListener('online', () => console.log('<<< console.log('<<<, - port: readonly MessagePort[] | undefined - ) => void ->; - -const messageHandlers: MessageHandlers = { - REQUEST_STATUS: () => self.status.sendCurrent(), - OPENED: () => localforage.setItem('started', { started: true }), -}; - -self.addEventListener('message', (e: ExtendableMessageEvent) => { - console.log('postMessage received', e); - - const msg = e.data as Message; - if (!ClientMessageType[msg.type]) { - console.warn('Ignoring client message with unknown type: ' + msg.type); - return; - } - messageHandlers[msg.type](msg, e.ports); -}); - -self.addEventListener('install', _event => { - // The promise that skipWaiting() returns can be safely ignored. - console.log('got install'); - self.skipWaiting(); - - // Perform any other actions required for your - // service worker to install, potentially inside - // of event.waitUntil(); - console.log('Skipped waiting'); -}); - -self.addEventListener('activate', async _event => { - console.log('got activate'); - // self.stashedFetch = self.fetch; - - // self.deferral = main().then(() => { - // console.log('patching fetch'); - // self.fetch = p2Fetch.bind(self); - // }).catch(e => { - // console.error(e) - // self.fetch = self.stashedFetch.bind(self) - // }); - - // self.fetch = async (...args) => { - // if (typeof args[0] === 'string') { - // return self.stashedFetch(...args); - // } - // console.log('fetch waiting for deferral', args[0]); - // await self.deferral; - // console.log('fetch deferred', args[0]); - // return self.fetch(...args); - // }; - await self.clients.claim(); - - // send status update to our client - self.status.sendCurrent(); - - console.log('Finish clients claim'); -}); - -self.stashedFetch = self.fetch; - -self.deferral = main() - .then(() => { - console.log('patching fetch'); - self.fetch = p2Fetch.bind(self); - }) - .catch(e => { - console.error(e); - self.fetch = self.stashedFetch.bind(self); - }); - -self.fetch = async (...args) => { - if (typeof args[0] === 'string') { - return self.stashedFetch(...args); - } - console.log('fetch waiting for deferral', args[0]); - - // Safari iOS needs a kick in the pants when PWA is installed - // very hard to debug what's going wrong because impossible - // to attach devtools to the service worker of an installed PWA - // but this seems to fix it - - // However, it unfortunately broke the worker on Chrome due to some sort - // of a race condition between libp2p being created and this loop firing, - // so it is being commented out for now. - - // const whip = setTimeout(async () => { - // const bootstraplist = await getBootstrapList(); - // for (const ma of bootstraplist) { - // await self.libp2p?.dial(ma as unknown as PeerId).catch(e => null); - // } - // }, 100); - - await self.deferral; - - //clearTimeout(whip); - - console.log('fetch deferred', args[0]); - return self.fetch(...args); -}; - -console.log('end of worker/index.js'); diff --git a/packages/gateway-client/src/worker/logging/index.ts b/packages/gateway-client/src/worker/logging/index.ts new file mode 100644 index 0000000..3e6c0b7 --- /dev/null +++ b/packages/gateway-client/src/worker/logging/index.ts @@ -0,0 +1,115 @@ +import localforage from 'localforage'; +import logger, { LogLevelDesc, levels, Logger } from 'loglevel'; +import yaml from 'js-yaml'; + +import config from './logging.yaml'; + +const DEFAULT_LEVEL = levels.INFO; + +const getDefaultLevel = (name: string) => { + for (const [matcher, level] of [...defaultLoggers.entries()].reverse()) { + if (matcher.test(name)) { + return level; + } + } + return DEFAULT_LEVEL; +}; + +const createNameMatcher = (name?: string) => new RegExp(name ?? '.*'); + +type LoggingConfig = { + loggers: Record; +}; + +// parse logging config +const loggingConfig = config ? (yaml.load(config) as LoggingConfig) : null; +const defaultLoggers = new Map( + Object.entries(loggingConfig?.loggers ?? {}).map(([name, level]) => [ + createNameMatcher(name), + level, + ]) +); + +const loadPersistedLevel = (name: string, logger: Logger) => { + localforage.getItem(`loglevel:${name}`).then(persisted => { + if (persisted) { + logger.setLevel(persisted as LogLevelDesc); + } + }); +}; + +// format our logging output +const originalFactory = logger.methodFactory; +logger.methodFactory = function (methodName, logLevel, loggerName) { + const originalMethodName = methodName; + // don't use console.trace() + if (originalMethodName === 'trace') { + methodName = 'debug'; + } + const rawMethod = originalFactory(methodName, logLevel, loggerName); + + const color = `hsl(${Math.random() * 360}, 100%, 40%)`; + + return function (...args) { + rawMethod( + `%c${originalMethodName.toUpperCase()} [${ + loggerName?.toString() ?? 'root' + }]`, + `color: ${color}`, + ...args + ); + }; +}; +// Be sure to call setLevel method in order to apply plugin +logger.setLevel(logger.getLevel()); + +// inherit default level from root logger +const originalGetLogger = logger.getLogger; +logger.getLogger = (name: string) => { + const isNew = !Object.prototype.hasOwnProperty.call( + logger.getLoggers(), + name + ); + const childLogger = originalGetLogger.call(logger, name); + childLogger.setDefaultLevel(getDefaultLevel(name)); + if (isNew) { + loadPersistedLevel(name, childLogger); + } + return childLogger; +}; + +// set root default level +logger.setDefaultLevel(getDefaultLevel('root')); +// load root persisted level +loadPersistedLevel('root', logger); + +export const getLoggers = (name?: string) => { + // construct regexp from given name to match loggers with + const regexp = createNameMatcher(name); + // get all loggers + return ( + Object.entries({ ...logger.getLoggers(), root: logger }) + // filter by name + .filter(([name]) => regexp.test(name)) + ); +}; + +export const resetLevel = (name?: string) => + getLoggers(name).forEach(([name, logger]) => { + logger.resetLevel(); + localforage.removeItem(`loglevel:${name}`); + }); + +export const setLevel = ( + levelOrName: LogLevelDesc | string, + level?: LogLevelDesc +) => { + const name = level ? (levelOrName as string) : undefined; + const newLevel = level ?? (levelOrName as LogLevelDesc); + getLoggers(name).forEach(([name, logger]) => { + logger.setLevel(newLevel); + localforage.setItem(`loglevel:${name}`, newLevel); + }); +}; + +export { logger }; diff --git a/packages/gateway-client/src/worker/logging/logging.spec.ts b/packages/gateway-client/src/worker/logging/logging.spec.ts new file mode 100644 index 0000000..01f812f --- /dev/null +++ b/packages/gateway-client/src/worker/logging/logging.spec.ts @@ -0,0 +1,66 @@ +import { logger, getLoggers, setLevel, resetLevel } from '.'; + +describe('logging should', () => { + afterEach(() => { + Object.entries(logger.getLoggers()).forEach(([name]) => { + delete logger.getLoggers()[name]; + }); + }); + + it('get all loggers', () => { + logger.getLogger('test'); + + expect(getLoggers()).toHaveLength(2); + }); + + it('get loggers by name', () => { + logger.getLogger('test'); + + expect(getLoggers('foo')).toHaveLength(0); + expect(getLoggers('test')).toHaveLength(1); + }); + + it('get loggers by name with wildcard', () => { + logger.getLogger('test'); + + expect(getLoggers('foo*')).toHaveLength(0); + expect(getLoggers('tes*')).toHaveLength(1); + }); + + it('set level', () => { + setLevel('trace'); + expect(logger.getLevel()).toBe(0); + setLevel('debug'); + expect(logger.getLevel()).toBe(1); + setLevel('info'); + expect(logger.getLevel()).toBe(2); + setLevel('warn'); + expect(logger.getLevel()).toBe(3); + setLevel('error'); + expect(logger.getLevel()).toBe(4); + setLevel('silent'); + expect(logger.getLevel()).toBe(5); + }); + + it('set level by name', () => { + logger.getLogger('foo'); + + setLevel('foo', 'trace'); + expect(getLoggers('foo')[0][1].getLevel()).toBe(0); + setLevel('foo', 'debug'); + expect(getLoggers('foo')[0][1].getLevel()).toBe(1); + setLevel('foo', 'info'); + expect(getLoggers('foo')[0][1].getLevel()).toBe(2); + setLevel('foo', 'warn'); + expect(getLoggers('foo')[0][1].getLevel()).toBe(3); + setLevel('foo', 'error'); + expect(getLoggers('foo')[0][1].getLevel()).toBe(4); + setLevel('foo', 'silent'); + expect(getLoggers('foo')[0][1].getLevel()).toBe(5); + }); + + it('reset level', () => { + resetLevel(); + expect(logger.getLevel()).toBe(2); + }); +}); diff --git a/packages/gateway-client/src/worker/logging/logging.yaml b/packages/gateway-client/src/worker/logging/logging.yaml new file mode 100644 index 0000000..19c110c --- /dev/null +++ b/packages/gateway-client/src/worker/logging/logging.yaml @@ -0,0 +1,5 @@ +loggers: + worker: INFO + worker/p2p/libp2p: ERROR + worker/p2p/bootstrap: DEBUG + worker/p2p/client: DEBUG diff --git a/packages/gateway-client/src/worker/logging/yaml.d.ts b/packages/gateway-client/src/worker/logging/yaml.d.ts new file mode 100644 index 0000000..2a4e822 --- /dev/null +++ b/packages/gateway-client/src/worker/logging/yaml.d.ts @@ -0,0 +1,4 @@ +declare module '*.yaml' { + const data: string; + export default data; +} diff --git a/packages/gateway-client/src/worker/messenger.spec.ts b/packages/gateway-client/src/worker/messenger.spec.ts new file mode 100644 index 0000000..551dfef --- /dev/null +++ b/packages/gateway-client/src/worker/messenger.spec.ts @@ -0,0 +1,26 @@ +import { Message, WorkerMessageType } from '../worker-messaging'; +import messenger from './messenger'; + +let mockClient = {} as WindowClient; + +jest.mock('./client', () => ({ + getBootstrapClient: jest.fn(() => Promise.resolve(mockClient)), +})); + +describe('messenger should', () => { + beforeEach(() => { + mockClient = { + postMessage: jest.fn(), + } as unknown as WindowClient; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('send a message', async () => { + const msg = { type: 'test' }; + await messenger.postMessage(msg as Message); + expect(mockClient.postMessage).toHaveBeenCalledWith(msg); + }); +}); diff --git a/packages/gateway-client/src/worker/messenger.ts b/packages/gateway-client/src/worker/messenger.ts new file mode 100644 index 0000000..72e6092 --- /dev/null +++ b/packages/gateway-client/src/worker/messenger.ts @@ -0,0 +1,57 @@ +import { + ClientMessageType, + Message, + WorkerMessageType, +} from '../worker-messaging'; +import { getBootstrapClient } from './client'; +import { logger } from './logging'; + +declare const self: ServiceWorkerGlobalScope; + +type MessageHandler = (msg: Message) => void; + +class Messenger { + private eventTarget = new EventTarget(); + private listeners: Map = new Map(); + private log = logger.getLogger('worker/messenger'); + + init() { + self.addEventListener('message', event => { + const msg = event.data; + this.log.debug('Received client message: ', msg); + this.eventTarget.dispatchEvent( + new CustomEvent(msg.type, { detail: msg }) + ); + }); + } + + addListener(type: K, handler: MessageHandler) { + const listener = ((event: CustomEvent>) => { + const msg = event.detail as Message; + handler(msg); + }) as EventListener; + + this.listeners.set(handler, listener); + + this.eventTarget.addEventListener(type, listener); + } + + removeListener( + type: K, + handler: MessageHandler + ) { + const listener = this.listeners.get(handler); + if (listener) { + this.eventTarget.removeEventListener(type, listener); + this.listeners.delete(handler); + } + } + + async postMessage(msg: Message) { + const client = await getBootstrapClient(); + this.log.debug('Sending worker message: ', msg); + client?.postMessage(msg); + } +} + +export default new Messenger(); diff --git a/packages/gateway-client/src/worker/migrations.spec.ts b/packages/gateway-client/src/worker/migrations.spec.ts new file mode 100644 index 0000000..2b756a1 --- /dev/null +++ b/packages/gateway-client/src/worker/migrations.spec.ts @@ -0,0 +1,7 @@ +import { runMigrations } from './migrations'; + +describe('migrations', () => { + it('should run without error', async () => { + await runMigrations(); + }); +}); diff --git a/packages/gateway-client/src/worker/migrations.ts b/packages/gateway-client/src/worker/migrations.ts new file mode 100644 index 0000000..18893a9 --- /dev/null +++ b/packages/gateway-client/src/worker/migrations.ts @@ -0,0 +1,100 @@ +import localforage from 'localforage'; + +import { logger } from './logging'; + +const log = logger.getLogger('worker/migrations'); + +type Migration = () => Promise; + +const migrations = new Map(); + +const registerMigration = (id: string, migration: Migration) => { + migrations.set(id, migration); +}; + +const migrationState = new Set(); + +const loadCache = async () => { + const cached = await localforage.getItem('migration:state'); + if (cached) { + JSON.parse(cached).forEach((id: string) => migrationState.add(id)); + } +}; + +const dumpCache = async () => { + await localforage.setItem( + 'migration:state', + JSON.stringify(Array.from(migrationState)) + ); +}; + +export const runMigrations = async () => { + // load migration data + await loadCache(); + + log.info('Running migrations...'); + + // loop migrations + for (const [id, migration] of migrations.entries()) { + // if this migration has already been executed + if (migrationState.has(id)) { + log.trace('Skipping already run migration: ', id); + // skip it + continue; + } + + // else, this migration has not yet been executed, so execute it + try { + log.info('Running migration: ', id); + await migration(); + } catch (e) { + log.error( + `Migration ${id} has failed (it will be re-run on next worker execution): `, + e + ); + continue; + } + + // if the migration was successful, add it to the cache + migrationState.add(id); + } + + log.info('Finished running migrations.'); + + // save migration data + await dumpCache(); +}; + +// Migrate from libp2p.bootstrap to new bootstrap-list +registerMigration('0f8edbd5-6aa0-492e-9c3a-b419752dfdb1', async () => { + // check for libp2p.bootstrap key + const oldBootstrap = await localforage.getItem( + 'libp2p.bootstrap' + ); + // if we didn't find it + if (!oldBootstrap) { + // no need to run the migration + return; + } // else, we need to migrate it + + // attempt to get the new bootstrap list, + // or create a new one if it doesn't exist + const newBootstrap = + (await localforage.getItem('p2p:bootstrap-list')) ?? '[]'; + const newBootstrapList = JSON.parse(newBootstrap) as Record< + string, + unknown + >[]; + // append the old bootstrap value to the new bootstrap list + newBootstrapList.push({ + address: oldBootstrap, + }); + // save the new bootstrap list + await localforage.setItem( + 'p2p:bootstrap-list', + JSON.stringify(newBootstrapList) + ); + + // we won't remove the old bootstrap key + // in case it is needed after the migration +}); diff --git a/packages/gateway-client/src/worker/p2p-client/bootstrap-list.ts b/packages/gateway-client/src/worker/p2p-client/bootstrap-list.ts new file mode 100644 index 0000000..2dbb068 --- /dev/null +++ b/packages/gateway-client/src/worker/p2p-client/bootstrap-list.ts @@ -0,0 +1,427 @@ +import { Bootstrap } from '@libp2p/bootstrap'; +import { MultiaddrConnection } from '@libp2p/interface-connection'; +import type { Address } from '@libp2p/interface-peer-store'; +import { Upgrader } from '@libp2p/interface-transport'; +import { peerIdFromString } from '@libp2p/peer-id'; +import { WebSockets } from '@libp2p/websockets'; +import { P2P } from '@multiformats/mafmt'; +import { multiaddr, Multiaddr } from '@multiformats/multiaddr'; +import { Buffer } from 'buffer'; +import { pipe } from 'it-pipe'; +import localforage from 'localforage'; + +import type { P2pClient } from '.'; +import { logger } from '../logging'; +import { nativeFetch } from '../p2p-fetch/override-fetch'; +import status from '../status'; + +const waitFor = async (t: number): Promise => + new Promise(r => setTimeout(r, t)); + +class BootstrapAddress { + public multiaddr: Multiaddr; + public lastSeen = 0; + public latency = Infinity; + public serverId: string; + public isRelay = false; + public isDNS = false; + + public constructor(public readonly address: string) { + if (!P2P.matches(address)) { + throw new Error('Invalid multiaddr'); + } + + this.multiaddr = multiaddr(address); + const serverId = this.multiaddr.getPeerId(); + if (!serverId) { + throw new Error( + `Address ${address} contains invalid or missing peer id.` + ); + } + + this.serverId = serverId; + this.isRelay = this.address.includes('/p2p-circuit/'); + this.isDNS = this.address.includes('/dns4/'); + } + + static fromJson(json: Record): BootstrapAddress { + const addr = new BootstrapAddress(json.address as string); + addr.lastSeen = json.lastSeen as number; + addr.latency = json.latency as number; + return addr; + } + + toJson(): Record { + return { + address: this.address ?? '', + lastSeen: this.lastSeen ?? Date.now(), + latency: this.latency ?? Infinity, + }; + } + + toString(): string { + return this.multiaddr.toString(); + } +} + +export class BootstrapList extends Bootstrap { + private log = logger.getLogger('worker/p2p/bootstrap'); + + private maxOffline = 7 * 24 * 60 * 60 * 1000; + private statsTimeout = 10000; + + private addresses: Record = {}; + private _serverId?: string; + + constructor(private client: P2pClient) { + // initialize bootstrap discovery with dummy list + super({ list: ['/ip4/1.2.3.4/tcp/1234/tls/p2p/QmFoo'] }); + // open the relay stream to receive new relay addresses + let relayDebounce = 0; + client.addEventListener('connected', () => { + if (Date.now() - relayDebounce > 60000) { + relayDebounce = Date.now(); + this.openRelayStream(); + } + }); + } + + private async populateStats(address: BootstrapAddress) { + // timeout after configured timeout + const abortController = new AbortController(); + const signal = abortController.signal; + waitFor(this.statsTimeout).then(() => abortController.abort()); + // send websocket request, track time + const start = Date.now(); + let socket; + try { + socket = await new WebSockets().dial( + address.isRelay + ? address.multiaddr.decapsulate('p2p-circuit') + : address.multiaddr, + { + signal, + upgrader: { + upgradeOutbound: async (socket: MultiaddrConnection) => + socket, + } as unknown as Upgrader, + } + ); + address.latency = Date.now() - start; + address.lastSeen = Date.now(); + } catch (e) { + this.log.debug(`Failed to connect to ${address}: `, e); + address.latency = Infinity; + } + // close the socket + try { + if (socket) { + await socket.close(); + } + } catch (e) { + this.log.warn(`Failed to close socket to ${address}: `, e); + } + // we've finished collecting stats + this.log.trace( + 'Latency for address: ', + address.address, + address.latency + ); + } + + private isRecent(address: BootstrapAddress) { + return address.lastSeen > Date.now() - this.maxOffline; + } + + private async addAddress(addressToAdd: string | BootstrapAddress) { + if (!addressToAdd) { + this.log.trace(`Ignoring falsy address: ${addressToAdd}`); + return null; + } + + // ensure this is a valid bootstrap address object + let address; + try { + address = + typeof addressToAdd === 'string' + ? new BootstrapAddress(addressToAdd) + : addressToAdd; + } catch (e) { + this.log.debug(`Ignoring invalid address: ${addressToAdd} (${e})`); + return null; + } + + // ensure this is a new address + if (this.addresses[address.address]) { + this.log.trace(`Declining to add existing address: ${address}`); + return null; + } + + // ensure it matches our current server id + if (this._serverId && address.serverId !== this._serverId) { + this.log.debug( + `Declining to add address with different server id: ${address}` + ); + return null; + } + + // get stats for this address + await this.populateStats(address); + // ensure this address is recent + if (!this.isRecent(address)) { + this.log.debug(`Declining to add stale address: ${address}`); + return null; + } + + // by this point, we know this is a valid and active address + // add this address to our list + this.log.trace(`Adding address: ${address}`); + this.addresses[address.address] = address; + return address; + } + + private async removeAddress(address: BootstrapAddress) { + this.log.trace(`Removing address: ${address}`); + delete this.addresses[address.address]; + } + + private async loadCache() { + // load our cached bootstrap list + const cached = await localforage.getItem('p2p:bootstrap-list'); + if (!cached) { + // no more to do + return; + } // else we have a cached list + const cacheList = JSON.parse(cached) as Record[]; + this.log.debug('Loaded cached bootstrap list: ', cacheList); + // parse the cached list + await Promise.all( + cacheList.map((address: Record) => { + let parsedAddress: string | BootstrapAddress = ''; + try { + parsedAddress = BootstrapAddress.fromJson(address); + } catch (e) { + this.log.warn('Invalid address in cache: ', address, e); + } + return this.addAddress(parsedAddress); + }) + ); + } + + private async dumpCache() { + // don't dump if the list is empty + if (!Object.keys(this.addresses).length) { + this.log.error('Declining to cache empty bootstrap list.'); + return; + } + // dump our bootstrap list to cache + return localforage.setItem( + 'p2p:bootstrap-list', + JSON.stringify( + Object.values(this.addresses).map(address => address.toJson()) + ) + ); + } + + private async openRelayStream() { + // open stream to relay protocol + const stream = await this.client.getStream('/samizdapp-relay'); + this.log.trace('Got relay stream: ', stream); + + // receive messages from relay protocol + await pipe(stream.source, async source => { + for await (const msg of source) { + // this message is an address + const addressString = Buffer.from(msg.subarray()).toString(); + this.log.debug( + `Received relay address from stream: ${addressString}` + ); + // add it to our list + const addedAddress = await this.addAddress(addressString); + // if NOT successfully added + if (!addedAddress) { + // nothing more to do + continue; + } + // else, we just added a new address + this.log.info(`Got new relay address: ${addedAddress}`); + // update our cache + await this.dumpCache(); + // add this new address to the client + this.client.addServerPeerAddress(addedAddress.multiaddr); + // update status + status.relays.push(addedAddress.address); + } + }).catch(e => { + this.log.warn('Error in pipe: ', e); + }); + // we wan't fetch streams to have priority, so let's ease up this loop + await new Promise(r => setTimeout(r, 20000)); + } + + public async refreshStats() { + // refresh the stats for all addresses + await Promise.all( + Object.values(this.addresses).map(async address => { + await this.populateStats(address); + // if this address is stale, remove it + if (!this.isRecent(address)) { + this.log.debug(`Removing stale address: ${address}`); + await this.removeAddress(address); + } + }) + ); + } + + public async load() { + // start by loading our cached bootstrap list + await this.loadCache(); + + // next, check for a new bootstrap address + let newBootstrapAddress = null; + try { + newBootstrapAddress = await nativeFetch( + '/smz/pwa/assets/libp2p.bootstrap' + ) + .then(res => { + if (res.status >= 400) { + throw res; + } + return res.text(); + }) + .then(text => text.trim()); + } catch (e) { + this.log.warn( + 'Error while trying to fetch new bootstrap address: ', + e + ); + } + // if we received a new bootstrap address, add it to our list + const addedBootstrap = await this.addAddress(newBootstrapAddress ?? ''); + // if it was added successfully + if (addedBootstrap) { + // our new bootstrap address was successfully added + this.log.info( + `Found updated bootstrap address, updating bootstrap list: ${addedBootstrap}` + ); + + // construct a /dns4 address from our new bootstrap address + const { hostname } = new URL(self.origin); + const [_, _proto, _ip, ...rest] = addedBootstrap.address.split('/'); + const withDns = `/dns4/${hostname}/${rest.join('/')}`; + // add it to our list + this.log.debug('Adding /dns4 address: ', withDns); + await this.addAddress(withDns); + + // construct a local /dns4 address + const withLocalDns = `/dns4/${hostname.replace( + /localhost$/, + 'local' + )}/${rest.join('/')}`; + // add it to our list + this.log.debug('Adding /dns4 local address: ', withLocalDns); + await this.addAddress(withLocalDns); + } + + // refresh stats + await this.refreshStats(); + + // log list + const addressList = this.addressList.map(it => it.address); + this.log.info('Loaded bootstrap addresses: ', addressList); + status.relays.push(...addressList); + + // if we have no addresses + if (!Object.keys(this.addresses).length) { + // this isn't good + this.log.error( + 'No addresses loaded into bootstrap list, client will fail.' + ); + return; + } + + // update our cache + await this.dumpCache(); + + // create a bootstrap discovery list grouped by peer + const addressesByPeer: Record = {}; + Object.values(this.addresses).forEach(address => { + if (!addressesByPeer[address.serverId]) { + addressesByPeer[address.serverId] = []; + } + addressesByPeer[address.serverId].push(address); + }); + // override our current bootstrap discovery list + Object.defineProperty(this, 'list', { + configurable: true, + value: Object.entries(addressesByPeer).map( + ([peerId, addresses]) => ({ + id: peerIdFromString(peerId), + multiaddrs: addresses.map(address => address.multiaddr), + protocols: [], + }) + ), + }); + + // get our server id + this._serverId = Object.values(this.addresses)[0]?.serverId; + } + + public get serverId() { + if (!this._serverId) { + throw new Error('Attempt to access serverId before it is set.'); + } + return this._serverId; + } + + private addressSorter(a: BootstrapAddress, b: BootstrapAddress) { + this.log.trace(`Sorting: ${a} <=> ${b}`); + + // if we don't have stats for an address, prefer the one we have stats for + if (!a && !b) { + return 0; + } + if (!a) { + return 1; + } + if (!b) { + return -1; + } + + // we have stats for both, prefer the one we've been able to connect to + if (a.latency === Infinity && b.latency !== Infinity) { + return 1; + } + if (a.latency !== Infinity && b.latency === Infinity) { + return -1; + } + + // we've been able to connect to both/neither, prefer a non relay + if (a.isRelay && !b.isRelay) { + return 1; + } + if (!a.isRelay && b.isRelay) { + return -1; + } + + // both/neither are relays, prefer the one with lower latency + return a.latency - b.latency; + } + + public libp2pAddressSorter(a: Address, b: Address) { + // first of all, get our addresses + const addressA = this.addresses[a.multiaddr.toString()]; + const addressB = this.addresses[b.multiaddr.toString()]; + return this.addressSorter(addressA, addressB); + } + + public all() { + return Object.values(this.addresses); + } + + public get addressList() { + return Object.values(this.addresses).sort((a, b) => + this.addressSorter(a, b) + ); + } +} diff --git a/packages/gateway-client/src/worker/p2p-client/index.ts b/packages/gateway-client/src/worker/p2p-client/index.ts new file mode 100644 index 0000000..af429d0 --- /dev/null +++ b/packages/gateway-client/src/worker/p2p-client/index.ts @@ -0,0 +1,305 @@ +import { Noise } from '@chainsafe/libp2p-noise'; +import { ConnectionEncrypter } from '@libp2p/interface-connection-encrypter'; +import { Connection } from '@libp2p/interface-connection'; +import { PeerId } from '@libp2p/interface-peer-id'; +import type { Address } from '@libp2p/interface-peer-store'; +import { KEEP_ALIVE } from '@libp2p/interface-peer-store/tags'; +import { StreamMuxerFactory } from '@libp2p/interface-stream-muxer'; +import { Mplex } from '@libp2p/mplex'; +import { WebSockets } from '@libp2p/websockets'; +import { all as filtersAll } from '@libp2p/websockets/filters'; +import { Multiaddr as MultiaddrType } from '@multiformats/multiaddr'; +import { createLibp2p, Libp2p } from 'libp2p'; + +import { ServerPeerStatus } from '../../worker-messaging'; +import { logger } from '../logging'; +import status from '../status'; +import { BootstrapList } from './bootstrap-list'; +import { initLibp2pLogging } from './libp2p-logging'; +import { StreamFactory } from './stream-factory'; + +const waitFor = async (t: number): Promise => + new Promise(r => setTimeout(r, t)); + +export class P2pClient { + private log = logger.getLogger('worker/p2p/client'); + + private DIAL_TIMEOUT = 3000; + + private streamFactory?: StreamFactory; + private bootstrapList: BootstrapList; + private eventTarget = new EventTarget(); + + private serverPeer?: PeerId; + private serverConnection?: Promise; + public node?: Libp2p; + + public constructor() { + this.bootstrapList = new BootstrapList(this); + } + + public async addServerPeerAddress(multiaddr: MultiaddrType) { + if (!this.serverPeer) { + throw new Error('No server peer discovered.'); + } + + await this.node?.peerStore.addressBook + .add(this.serverPeer, [multiaddr]) + .catch(_ => _); + } + + public getStream(protocol?: string, id?: string) { + if (!this.streamFactory) { + throw new Error('No connection established!'); + } + + return this.streamFactory.getStream(protocol, id); + } + + public async connectToServer(retryTimeout = 1000): Promise { + // if we haven't started yet + if (!this.node) { + throw new Error( + 'Connection attempted before P2P client was started!' + ); + } + + // we'll also need to have discovered our peer + if (!this.serverPeer) { + throw new Error( + 'Connection attempted before server peer discovered!' + ); + } + + // if we already have a connection (completed or pending) + if (this.serverConnection) { + // don't create a second one + return this.serverConnection; + } // else, we're good to go + + // first, close any open connections to our server + this.log.debug('Closing existing server connections...'); + await this.node.hangUp(this.serverPeer); + + // at some point, addresses for our peer can get removed + // re-add everything from our bootstrap list before + // trying to connect again + this.log.debug('Re-adding server peer addresses'); + this.node.peerStore.addressBook.add( + this.serverPeer, + this.bootstrapList.all().map(it => it.multiaddr) + ); + + // now, attempt to dial our server + this.log.info('Dialing server...'); + this.serverConnection = this.node + .dial(this.serverPeer) + .catch(async e => { + // we weren't able to dial + this.log.error('Error dialing server: ', e); + this.log.debug('Retrying dial in: ', retryTimeout); + this.serverConnection = undefined; + // wait before retrying + await waitFor(retryTimeout); + // refresh our stats so that the dial gets an updated order + await this.bootstrapList.refreshStats(); + // retry + this.log.info('Redialing server...'); + return this.connectToServer( + retryTimeout > 30000 ? retryTimeout : retryTimeout + 1000 + ); + }); + + // return our connection + return this.serverConnection; + } + + public async start() { + // load our bootstrap list + await this.bootstrapList.load(); + + // update status + status.serverPeer = ServerPeerStatus.BOOTSTRAPPED; + + // create libp2p node + this.node = await createLibp2p({ + // datastore, + transports: [ + new WebSockets({ + filter: filtersAll, + }), + ], + connectionEncryption: [ + new Noise() as unknown as ConnectionEncrypter, + ], + streamMuxers: [new Mplex() as StreamMuxerFactory], + peerDiscovery: [this.bootstrapList], + connectionManager: { + // current version of libp2p will still autodial unless + // explicitly disabled (removed in later version) + autoDial: false, + minConnections: 3, + maxDialsPerPeer: 20, + maxParallelDials: 20, + addressSorter: (a: Address, b: Address) => + this.bootstrapList.libp2pAddressSorter(a, b), + }, + relay: { + // Circuit Relay options (this config is part of libp2p core configurations) + // The circuit relay is a second transporter that is configured in libp2p (is tried after Websockets) + enabled: true, // Allows you to dial and accept relayed connections. Does not make you a relay. + autoRelay: { + enabled: true, // Allows you to bind to relays with HOP enabled for improving node dialability + maxListeners: 5, // Configure maximum number of HOP relays to use + }, + }, + identify: { + host: { + agentVersion: `smz-pwa/0.0.0`, + }, + }, + }); + + // init libp2p logging + initLibp2pLogging(); + + // add event listeners + + // track discovered peers + const discoveredPeers = new Set(); + this.node.addEventListener('peer:discovery', evt => { + // if this is a new peer that we've just discovered + // (as opposed to an existing peer that we *haven't* discovered, + // but libp2p is dispatching a discovery event for anyway, + // because that makes sense) + const peerId = evt.detail.id.toString(); + if (!discoveredPeers.has(peerId)) { + // we've discovered it + discoveredPeers.add(peerId); + this.log.info(`Discovered peer ${peerId.toString()}`); + // if this is our server peer + if (this.bootstrapList.serverId === peerId) { + this.serverPeer = evt.detail.id; + // connect to our server (autodial is disabled) + this.connectToServer(); + } + } + }); + + // Listen for new connections to peers + this.node.connectionManager.addEventListener( + 'peer:connect', + async evt => { + try { + // log connection details + const connection = evt.detail; + const str_id = connection.remotePeer.toString(); + this.log.info(`Connected to: `, { + server: str_id, + via: connection.remoteAddr.toString(), + }); + + // if this is not our server + const serverMatch = str_id === this.bootstrapList.serverId; + this.log.info( + `Server match: ${serverMatch} (${str_id} ${ + serverMatch ? '=' : '!' + }== ${this.bootstrapList.serverId})` + ); + if (!serverMatch) { + // then there is no more to do + return; + } + + // else, we've connected to our server + this.log.info('Connected to server.'); + + if (!this.serverConnection) { + this.serverConnection = Promise.resolve(connection); + } + this.streamFactory = new StreamFactory( + this.DIAL_TIMEOUT, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.serverPeer!, + this + ); + + // update status + status.serverPeer = ServerPeerStatus.CONNECTED; + + // set keep-alive on connection + try { + await this.node?.peerStore + .tagPeer(connection.remotePeer, KEEP_ALIVE) + .catch(_ => null); + } catch (e) { + // ignore tagging errors + } + + this.dispatchEvent('connected'); + } catch (e) { + this.dispatchEvent('connectionerror', e); + } + } + ); + + // Listen for peers disconnecting + this.node.connectionManager.addEventListener('peer:disconnect', evt => { + const connection = evt.detail; + this.log.info( + `Disconnected from ${connection.remotePeer.toString()}` + ); + if ( + this.serverPeer && + connection.remotePeer.equals(this.serverPeer) + ) { + this.log.warn('Disconnected from server.'); + this.serverConnection = undefined; + // update status + status.serverPeer = ServerPeerStatus.CONNECTING; + this.connectToServer(); + } + }); + + // time to start up our node + this.log.debug('Starting libp2p...'); + await this.node.start(); + this.log.info('Started libp2p.'); + + // update status + status.serverPeer = ServerPeerStatus.CONNECTING; + waitFor(15000).then(() => { + if (status.serverPeer === ServerPeerStatus.CONNECTING) { + status.serverPeer = ServerPeerStatus.OFFLINE; + } + }); + } + + private dispatchEvent(type: string, detail?: T) { + this.eventTarget.dispatchEvent(new CustomEvent(type, { detail })); + } + + public addEventListener( + type: string, + listener: (evt: CustomEvent) => void, + options?: boolean | AddEventListenerOptions + ): void { + this.eventTarget.addEventListener( + type, + listener as EventListener, + options + ); + } + + public removeEventListener( + type: string, + listener: (evt: CustomEvent) => void, + options?: boolean | EventListenerOptions + ): void { + this.eventTarget.removeEventListener( + type, + listener as EventListener, + options + ); + } +} diff --git a/packages/gateway-client/src/worker/p2p-client/libp2p-logging.ts b/packages/gateway-client/src/worker/p2p-client/libp2p-logging.ts new file mode 100644 index 0000000..8b31ea6 --- /dev/null +++ b/packages/gateway-client/src/worker/p2p-client/libp2p-logging.ts @@ -0,0 +1,41 @@ +import * as libp2pLogger from '@libp2p/logger'; +import { levels, LogLevelDesc, LogLevelNumbers } from 'loglevel'; + +import { logger } from '../logging'; + +const log = logger.getLogger('worker/p2p/libp2p'); + +// pass logger levels down to libp2p logger +const levelHandlers: Record void> = { + [levels.SILENT]: () => libp2pLogger.disable(), + [levels.ERROR]: extra => + libp2pLogger.enable( + 'libp2p:circuit:error, libp2p:bootstrap:error, ' + extra + ), + [levels.WARN]: extra => + levelHandlers[levels.ERROR]( + 'libp2p:websockets:error, libp2p:upgrader:error, ' + extra + ), + [levels.INFO]: extra => + levelHandlers[levels.WARN]( + 'libp2p:dialer:error, libp2p:connection-manager:trace, ' + extra + ), + [levels.DEBUG]: extra => + levelHandlers[levels.INFO]( + 'libp2p:peer-store:trace, libp2p:mplex:stream:trace, libp2p:*:error, ' + + extra + ), + [levels.TRACE]: extra => + levelHandlers[levels.DEBUG]('libp2p:*:trace, ' + extra), +}; + +// customize setLevel functionality +const originalLogSetLevel = log.setLevel; +log.setLevel = (level: LogLevelDesc) => { + originalLogSetLevel.call(log, level); + levelHandlers[log.getLevel()](); +}; + +export const initLibp2pLogging = () => { + log.setLevel(log.getLevel()); +}; diff --git a/packages/gateway-client/src/worker/p2p-client/stream-factory.ts b/packages/gateway-client/src/worker/p2p-client/stream-factory.ts new file mode 100644 index 0000000..9009e4a --- /dev/null +++ b/packages/gateway-client/src/worker/p2p-client/stream-factory.ts @@ -0,0 +1,145 @@ +import { Stream } from '@libp2p/interface-connection'; +import { PeerId } from '@libp2p/interface-peer-id'; + +import type { P2pClient } from '.'; +import { ServerPeerStatus } from '../../worker-messaging'; +import { logger } from '../logging'; +import status from '../status'; + +const waitFor = async (t: number): Promise => + new Promise(r => setTimeout(r, t)); + +export class StreamFactory { + private log = logger.getLogger('worker/p2p/stream'); + + private inTimeout = false; + private retryTimeout = 0; + private dialTimeout: number; + + public constructor( + private maxDialTimeout: number, + private serverPeer: PeerId, + private client: P2pClient + ) { + this.dialTimeout = maxDialTimeout; + } + + private async makeStream(protocol: string, _piperId: string) { + this.log.trace('Get stream for protocol: ', protocol); + + // we need to be connected + if (!this.client.node) { + throw new Error('No client connection established!'); + } + + // start with no stream + let stream: Stream | null = null; + while (!stream) { + // if we're currently in timeout, wait until we're not + while (this.inTimeout) { + this.log.trace('Waiting for timeout to end...'); + await waitFor(100); + } + + // attempt to dial our peer, track the time it takes + const start = Date.now(); + // timeout after configured timeout + const abortController = new AbortController(); + const signal = abortController.signal; + waitFor(this.dialTimeout).then(() => abortController.abort()); + // initiate dial + stream = await this.client.node + .dialProtocol(this.serverPeer, protocol, { signal }) + .catch(e => { + this.log.trace('dialProtocol error: ', e); + this.log.trace('Time: ', Date.now() - start); + return null; + }); + + // if we successfully opened a stream + if (stream) { + // reset our timeouts + this.retryTimeout = 0; + // use the time of the dial to calculate an + // appropriate dial timeout + this.dialTimeout = Math.max( + this.maxDialTimeout, + Math.floor((Date.now() - start) * 4) + ); + + // if we were NOT previously connected + if (status.serverPeer !== ServerPeerStatus.CONNECTED) { + // we are now + status.serverPeer = ServerPeerStatus.CONNECTED; + } + // we have a stream, so we can quit now + this.log.trace('Got stream for protocol: ', protocol, stream); + break; + } + + // if we're currently in timeout + if (this.inTimeout) { + // we're already in timeout, so there is nothing more to do + this.log.trace('Already in timeout, waiting...'); + continue; + } + + // else, we need to put ourselves in timeout + // not only are we going to wait for the specified timeout period, + // but we will also force any concurrent calls to wait as well + this.inTimeout = true; + + // else, we failed to open a stream + // this is a connection error, if we were previously connected + if (status.serverPeer === ServerPeerStatus.CONNECTED) { + // we aren't anymore + status.serverPeer = ServerPeerStatus.CONNECTING; + } + + // if our retry timeout reaches 5 seconds, then we'll have + // been retrying for 15 seconds (triangle number of 5). + // By this point, we're probably offline. + if ( + status.serverPeer !== ServerPeerStatus.OFFLINE && + this.retryTimeout >= 5000 + ) { + status.serverPeer = ServerPeerStatus.OFFLINE; + } + + // wait awhile before retrying the stream again + this.log.info('Dial timeout, waiting to reset...', { + dialTimeout: this.dialTimeout, + retryTimeout: this.retryTimeout, + }); + await waitFor(this.retryTimeout); + + // if our retry timeout reaches 30 seconds, then we'll have + // been retrying for 5 minutes 45 seconds + // (triangle number of 30) + // time to reset + if (this.retryTimeout >= 30000) { + this.retryTimeout = 0; + } + // increase our retry timeout + this.retryTimeout += 1000; + // increase our dial timeout, but never make it higher than + // 5 minutes + this.dialTimeout = Math.min(1000 * 60 * 5, this.dialTimeout * 4); + + // now that we've waited awhile, we can attempt to reconnect to our server + await this.client.connectToServer(); + // and try again + this.inTimeout = false; + } + + // our loop will continue until we have a stream, which we now do + return stream; + } + + public async getStream( + protocol = '/samizdapp-proxy', + id: string = crypto.randomUUID() + ) { + return this.makeStream(protocol, id); + } +} diff --git a/packages/gateway-client/src/worker/p2p-fetch/override-fetch.ts b/packages/gateway-client/src/worker/p2p-fetch/override-fetch.ts new file mode 100644 index 0000000..a848e77 --- /dev/null +++ b/packages/gateway-client/src/worker/p2p-fetch/override-fetch.ts @@ -0,0 +1,306 @@ +import { Buffer } from 'buffer/'; +import { pipe } from 'it-pipe'; +import type * as it from 'it-stream-types'; +import { decode, encode } from 'lob-enc'; + +import { ServerPeerStatus } from '../../worker-messaging'; +import { isBootstrapAppUrl } from '../client'; +import { logger } from '../logging'; +import { P2pClient } from '../p2p-client'; +import status from '../status'; + +const log = logger.getLogger('worker/p2p-fetch/override-fetch'); + +type GlobalSelf = { + soapstore: LocalForage; + DIAL_TIMEOUT: number; + deferral: Promise; + stashedFetch: typeof fetch; + Buffer: typeof Buffer; + _fetch: typeof fetch; + latencyMap: Map; + latencySet: Set; + pipersInProgress: Map>; +}; + +let p2pClient: P2pClient; + +const CHUNK_SIZE = 1024 * 64; + +const globalSelf = {} as GlobalSelf; + +globalSelf.pipersInProgress = new Map(); + +const waitFor = async (t: number): Promise => + new Promise(r => setTimeout(r, t)); + +async function normalizeBody(body: unknown) { + try { + if (!body) return undefined; + if (typeof body === 'string') return Buffer.from(body); + if (Buffer.isBuffer(body)) return body; + if (body instanceof ArrayBuffer) { + if (body.byteLength > 0) return Buffer.from(new Uint8Array(body)); + return undefined; + } + type WithArrayBuffer = { arrayBuffer: () => Promise }; + if ((body as WithArrayBuffer).arrayBuffer) { + return Buffer.from( + new Uint8Array(await (body as WithArrayBuffer).arrayBuffer()) + ); + } + if ((body as ReadableStream).toString() === '[object ReadableStream]') { + const reader = (body as ReadableStream).getReader(); + const chunks = []; + let _done = false; + do { + const { done, value } = await reader.read(); + _done = done; + chunks.push(Buffer.from(new Uint8Array(value))); + } while (!_done); + return Buffer.concat(chunks); + } + + throw new Error(`don't know how to handle body`); + } catch (e) { + return Buffer.from( + `${(e as Error).message} ${typeof body} ${( + body as string + ).toString()} ${JSON.stringify(body)}` + ); + } +} + +async function p2Fetch( + givenReqObj: URL | RequestInfo, + givenReqInit: RequestInit | undefined = {}, + _xhr?: XMLHttpRequest +): Promise { + // assert that we were given a request + givenReqObj = givenReqObj as Request; + + if (typeof givenReqObj.url != 'string') { + throw new Error( + `Patched service worker \`fetch()\` method expects a full request object, received ${givenReqObj.constructor.name}` + ); + } + + // patch args + const body = + givenReqObj.body ?? + givenReqInit.body ?? + (await givenReqObj.arrayBuffer?.()) ?? + null; + const { reqObj, reqInit } = patchFetchArgs(givenReqObj, givenReqInit); + + // apply filtering to the request + const url = new URL( + reqObj.url.startsWith('http') + ? reqObj.url + : `http://localhost${reqObj.url}` + ); + + if (process.env.NX_LOCAL === 'true' && isBootstrapAppUrl(url)) { + return nativeFetch(givenReqObj, givenReqInit); + } + + if (url.pathname.startsWith('/smz')) { + reqObj.headers['X-Intercepted-Subdomain'] = 'samizdapp'; + } else if (url.pathname !== '/manifest.json') { + reqObj.headers['X-Intercepted-Subdomain'] = 'pleroma'; + } + + if (url.host === getHost()) { + url.host = 'localhost'; + url.protocol = 'http:'; + url.port = '80'; + } + + reqObj.url = url.toString(); + + // console.log("pocketFetch2", reqObj, reqInit, body); + //delete (reqObj as Request).body; + delete reqInit?.body; + const pbody = await normalizeBody(body); + const packet = encode({ reqObj, reqInit }, pbody); + // console.log('packet:', packet.toString('hex')) + let i = 0; + const parts: Buffer[] = []; + for (; i <= Math.floor(packet.length / CHUNK_SIZE); i++) { + parts.push( + packet.slice( + i * CHUNK_SIZE, + (i + 1) * CHUNK_SIZE + ) as unknown as Buffer + ); + } + + parts.push(Buffer.from([0x00])); + // console.log('packet?', packet) + // console.log('get fetch stream'); + + // console.log('parts:') + // parts.forEach(p => console.log(p.toString('hex'))) + // let j = 0; + let done = false; + let res_parts: Buffer[] = []; + let floatMax = parts.length * 5000; + + async function piper(piperId: string) { + const st = Date.now(); + const stream = await p2pClient.getStream('/samizdapp-proxy', piperId); + console.log('time to stream', Date.now() - st); + + let float = 0, + t = Date.now(), + gotFirstChunk = false; + // console.log('piper parts', parts); + pipe( + parts, + stream as unknown as it.Duplex, + async function gatherResponse(source) { + for await (const msg of source) { + if (gotFirstChunk) { + float = Math.max(float, Date.now() - t); + } else { + console.log('time to first chunk', Date.now() - t); + gotFirstChunk = true; + } + t = Date.now(); + const buf = Buffer.from(msg.subarray()); + if (msg.subarray().length === 1 && buf[0] === 0x00) { + done = true; + } else { + res_parts.push(buf); + } + } + } + ); + + while (!done) { + await waitFor(100); + if (float && Date.now() - t > Math.max(500, float * 4)) { + console.log('time float', float); + break; + } + if (!float && Date.now() - t > floatMax) { + console.log('time floatMax', floatMax); + floatMax += parts.length * 5000; + break; + } + if (status.serverPeer !== ServerPeerStatus.CONNECTED) { + console.log('time serverPeer', status.serverPeer); + break; + } + } + + stream.close(); + // console.log('piper finish'); + } + + let j = 0; + while (!done) { + console.log('try', j++, reqObj.url); + res_parts = []; + const piperId = crypto.randomUUID(); + const piperProm = piper(piperId); + globalSelf.pipersInProgress.set(piperId, piperProm); + await piperProm; + globalSelf.pipersInProgress.delete(piperId); + // await piper(); + console.log('time done', done); + } + + const resp = decode(Buffer.concat(res_parts)); + if (!resp.json.res) { + throw resp.json.error; + } + resp.json.res.headers = new Headers(resp.json.res.headers); + // alert("complete"); + return new Response(resp.body, resp.json.res); +} + +const getHost = () => { + try { + return window.location.host; + } catch (e) { + return self.location.host; + } +}; + +function patchFetchArgs(_reqObj: Request, _reqInit: RequestInit = {}) { + // console.log("patch"); + + const rawHeaders = Object.fromEntries(_reqObj.headers.entries()); + + const reqObj = { + bodyUsed: _reqObj.bodyUsed, + cache: _reqObj.cache, + credentials: _reqObj.credentials, + destination: _reqObj.destination, + headers: rawHeaders, + integrity: _reqObj.integrity, + isHistoryNavigation: ( + _reqObj as Request & { isHistoryNavigation: boolean } + ).isHistoryNavigation, + keepalive: _reqObj.keepalive, + method: _reqObj.method, + mode: _reqObj.mode, + redirect: _reqObj.redirect, + referrer: _reqObj.referrer, + referrerPolicy: _reqObj.referrerPolicy, + url: _reqObj.url, + }; + + const reqInit = { + ..._reqInit, + headers: rawHeaders, + }; + + return { reqObj, reqInit }; +} + +export const nativeFetch = self.fetch; + +export const overrideFetch = (client: P2pClient) => { + p2pClient = client; + + // track client connection + type ConnectionStatus = 'connecting' | 'connected' | 'disconnected'; + let connectionStatus: ConnectionStatus = 'connecting'; + p2pClient.addEventListener('connected', () => { + connectionStatus = 'connected'; + log.info('Client connected'); + }); + p2pClient.addEventListener('connectionerror', e => { + connectionStatus = 'disconnected'; + log.error('Client connection error: ', e.detail); + }); + + // override fetch + self.fetch = async (...args) => { + // if we are connected, use p2p fetch + if (connectionStatus === 'connected') { + log.trace('Using p2p fetch: ', args[0]); + return p2Fetch(...args); + } + + // else, if we are disconnected, use native fetch + if (connectionStatus === 'disconnected') { + log.trace('Using native fetch: ', args[0]); + return nativeFetch(...args); + } + + // else, we are still connecting, wait for connection + log.info('Waiting for client connection, fetch deferred...', args[0]); + return new Promise(resolve => { + const handler = () => { + p2pClient.removeEventListener('connected', handler); + // try again + log.info('Retrying deferred fetch...', args[0]); + resolve(self.fetch(...args)); + }; + p2pClient.addEventListener('connected', handler); + }); + }; +}; diff --git a/packages/gateway-client/src/worker/service-worker.ts b/packages/gateway-client/src/worker/service-worker.ts new file mode 100644 index 0000000..011158f --- /dev/null +++ b/packages/gateway-client/src/worker/service-worker.ts @@ -0,0 +1,45 @@ +// The workbox-precaching import includes a type definition for +// . +// Import it even though we're not using any of the imports, +import * as workboxPrecaching from 'workbox-precaching'; + +import bootstrap from './bootstrap'; +import { + passThroughHandler, + pleromaTimelineHandler, + staticCacheHandler, +} from './fetch-handlers'; +import fetchHandlers from './fetch-handlers/fetch-handlers'; +import { logger } from './logging'; + +// Mark the workbox-precaching import as being used with this line: +const _ = workboxPrecaching; + +declare const self: ServiceWorkerGlobalScope; + +const log = logger.getLogger('worker/main'); + +log.info('Executing worker...'); + +// To disable all workbox logging during development, you can set self.__WB_DISABLE_DEV_LOGS to true +// https://developers.google.com/web/tools/workbox/guides/configure-workbox#disable_logging +// self.__WB_DISABLE_DEV_LOGS = true + +const WB_MANIFEST = self.__WB_MANIFEST; + +// Precache all of the assets generated by your build process. +// Their URLs are injected into the manifest variable below. +// This variable must be present somewhere in your service worker file, +// even if you decide not to use precaching. See https://cra.link/PWA +//precacheAndRoute(WB_MANIFEST); + +log.trace(WB_MANIFEST); + +fetchHandlers + .use(staticCacheHandler) + .use(pleromaTimelineHandler) + .use(passThroughHandler); + +bootstrap(); + +log.info('Worker executed.'); diff --git a/packages/gateway-client/src/worker/status.spec.ts b/packages/gateway-client/src/worker/status.spec.ts new file mode 100644 index 0000000..ecdff2e --- /dev/null +++ b/packages/gateway-client/src/worker/status.spec.ts @@ -0,0 +1,7 @@ +import status from './status'; + +describe('status should', () => { + it('be initialized', () => { + expect(status).toBeDefined(); + }); +}); diff --git a/packages/gateway-client/src/worker/status.ts b/packages/gateway-client/src/worker/status.ts new file mode 100644 index 0000000..4b20565 --- /dev/null +++ b/packages/gateway-client/src/worker/status.ts @@ -0,0 +1,53 @@ +import { + ClientMessageType, + ServerPeerStatus, + WorkerMessageType, +} from '../worker-messaging'; +import messenger from './messenger'; + +class Status { + _serverPeer: ServerPeerStatus | null = null; + relays: string[] = []; + + constructor() { + messenger.addListener(ClientMessageType.REQUEST_STATUS, () => { + this.sendCurrent(); + }); + + Reflect.defineProperty(this.relays, 'push', { + value: (...items: string[]): number => { + const ret = Array.prototype.push.call(this.relays, ...items); + messenger.postMessage({ + type: WorkerMessageType.LOADED_RELAYS, + relays: this.relays, + }); + return ret; + }, + }); + } + + get serverPeer() { + return this._serverPeer; + } + + set serverPeer(status: ServerPeerStatus | null) { + this._serverPeer = status; + messenger.postMessage({ + type: WorkerMessageType.SERVER_PEER_STATUS, + status, + }); + } + + async sendCurrent() { + messenger.postMessage({ + type: WorkerMessageType.LOADED_RELAYS, + relays: this.relays, + }); + messenger.postMessage({ + type: WorkerMessageType.SERVER_PEER_STATUS, + status: this.serverPeer, + }); + } +} + +export default new Status(); diff --git a/packages/gateway-client/webpack.config.js b/packages/gateway-client/webpack.config.js index f975f38..8239d5f 100644 --- a/packages/gateway-client/webpack.config.js +++ b/packages/gateway-client/webpack.config.js @@ -36,6 +36,10 @@ module.exports = (config, _context) => { } return it; }), + { + test: /\.yaml$/, + type: 'asset/source', + }, ], }, node: { @@ -58,7 +62,7 @@ module.exports = (config, _context) => { ), new NodePolyfillPlugin(), new InjectManifest({ - swSrc: 'packages/gateway-client/src/worker/index.ts', + swSrc: 'packages/gateway-client/src/worker/service-worker.ts', swDest: 'service-worker.js', }), ],