diff --git a/.gitignore b/.gitignore index bb1dae1..2545672 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ node_modules -/.env -/.DS_Store +.env +.DS_Store + /coverage /.github_old diff --git a/.nvmrc b/.nvmrc index b009dfb..2bf5ad0 100755 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -lts/* +stable diff --git a/eslint.config.js b/eslint.config.js index bbb9706..4bdceec 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -23,7 +23,8 @@ export default [ languageOptions: { globals: { ...globals.browser, - expect: 'readonly' + expect: 'readonly', + global: 'readonly' } }, plugins: { diff --git a/package-lock.json b/package-lock.json index 8a1bf61..18766a4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2820,6 +2820,15 @@ "node": ">=12" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -3317,6 +3326,21 @@ "dev": true, "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "12.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", @@ -3561,6 +3585,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cssstyle": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.1.0.tgz", + "integrity": "sha512-h66W1URKpBS5YMI/V8PyXvTMFT8SupJ1IzoIV8IeBC/ji8WVmrO8dGlTi+2dh6whmdk6BiKJLD/ZBkhWbcg6nA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "rrweb-cssom": "^0.7.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/dargs": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/dargs/-/dargs-8.1.0.tgz", @@ -3574,6 +3613,44 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/data-urls/node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=18" + } + }, "node_modules/debug": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", @@ -3592,6 +3669,15 @@ } } }, + "node_modules/decimal.js": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/deep-eql": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", @@ -3689,6 +3775,18 @@ "node": ">=8" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -4669,6 +4767,30 @@ "reusify": "^1.0.4" } }, + "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==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/fetch-mock": { "version": "11.1.3", "resolved": "https://registry.npmjs.org/fetch-mock/-/fetch-mock-11.1.3.tgz", @@ -4842,6 +4964,36 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/from2": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", @@ -5248,6 +5400,21 @@ "node": "^16.14.0 || >=18.0.0" } }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -5309,6 +5476,21 @@ "url": "https://github.com/sponsors/typicode" } }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -5562,6 +5744,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -5749,6 +5940,76 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "25.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-25.0.1.tgz", + "integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "cssstyle": "^4.1.0", + "data-urls": "^5.0.0", + "decimal.js": "^10.4.3", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.12", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.7.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.0.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^2.11.2" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/parse5": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.0.tgz", + "integrity": "sha512-ZkDsAOcxsUMZ4Lz5fVciOehNcJ+Gb8gTzcA4yl3wnc273BAybYWrQ+Ks/OjCjSEpjvQkDSeZbybK9qj2VHHdGA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "entities": "^4.5.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/jsdom/node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=18" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -6440,6 +6701,33 @@ "node": ">=16" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -6562,6 +6850,26 @@ "dev": true, "license": "MIT" }, + "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==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, "node_modules/node-emoji": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-2.1.3.tgz", @@ -6578,6 +6886,25 @@ "node": ">=18" } }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dev": true, + "license": "MIT", + "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/normalize-package-data": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz", @@ -9161,6 +9488,15 @@ "inBundle": true, "license": "ISC" }, + "node_modules/nwsapi": { + "version": "2.2.13", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.13.tgz", + "integrity": "sha512-cTGB9ptp9dY9A5VbMSe7fQBcl/tt22Vcqdq8+eN93rblOuE0aCFu4aZ2vMwct/2t+lFnosm8RkQW1I0Omb1UtQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -10146,6 +10482,15 @@ "fsevents": "~2.3.2" } }, + "node_modules/rrweb-cssom": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", + "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -10196,6 +10541,30 @@ "regexp-tree": "~0.1.1" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/semantic-release": { "version": "24.1.1", "resolved": "https://registry.npmjs.org/semantic-release/-/semantic-release-24.1.1.tgz", @@ -11239,6 +11608,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/synckit": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.1.tgz", @@ -11473,6 +11851,30 @@ "node": ">=14.0.0" } }, + "node_modules/tldts": { + "version": "6.1.53", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.53.tgz", + "integrity": "sha512-4uCStuOjPFaY2/LUjTSwdnJTC82W/gvSFL6FoTC9ehNOHboA9cyO3wX1erh2yGofVls37OdXr5sQLEfL5hS1TA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "tldts-core": "^6.1.53" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.53", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.53.tgz", + "integrity": "sha512-IleS872aGdTB/UtocD2dSZBnQi/nqMIZxxezVgfcKKjw6+G2hJGzFw9buIDJO2MVJyEJe3rCAdyMTl2yvGMMrQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", @@ -11496,6 +11898,36 @@ "node": ">=8.0" } }, + "node_modules/tough-cookie": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.0.0.tgz", + "integrity": "sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "peer": true, + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", + "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/traverse": { "version": "0.6.8", "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.8.tgz", @@ -11831,6 +12263,31 @@ } } }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", @@ -11841,6 +12298,21 @@ "node": ">=12" } }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/whatwg-mimetype": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", @@ -11851,6 +12323,22 @@ "node": ">=12" } }, + "node_modules/whatwg-url": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.0.0.tgz", + "integrity": "sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "tr46": "^5.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -12042,6 +12530,51 @@ "dev": true, "license": "ISC" }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -12178,6 +12711,9 @@ "dependencies": { "@rxjs-collection/observables": "*", "rxjs": "7.8.1" + }, + "devDependencies": { + "node-fetch": "^3.3.2" } } } diff --git a/packages/observables/.releaserc b/packages/observables/.releaserc index a90e7ef..fc40687 100644 --- a/packages/observables/.releaserc +++ b/packages/observables/.releaserc @@ -39,15 +39,7 @@ } ], [ - "@semantic-release/github", - { - "assets": [ - { - "path": "./packages/observables/", - "label": "observables" - } - ] - } + "@semantic-release/github" ] ], "extends": "semantic-release-monorepo" diff --git a/packages/observables/src/fetch/request.http b/packages/observables/src/fetch/request.http deleted file mode 100644 index 286a6eb..0000000 --- a/packages/observables/src/fetch/request.http +++ /dev/null @@ -1,9 +0,0 @@ - -# @name post -GET https://dummyjson.com/posts/1 HTTP/1.1 -content-type: application/json - -### -@userId = {{post.response.body.userId}} -GET https://dummyjson.com/users/{{userId}} HTTP/1.1 -content-type: application/json diff --git a/packages/observables/src/fetch/request.js b/packages/observables/src/fetch/request.js deleted file mode 100644 index 95598a9..0000000 --- a/packages/observables/src/fetch/request.js +++ /dev/null @@ -1,8 +0,0 @@ -import { fromFetch } from 'rxjs/fetch'; - -export const requestObservable = request => { - return fromFetch(request) - .pipe - // add operators for default behaviour - (); -}; diff --git a/packages/operators/fixtures/images/test_image.jpg b/packages/operators/fixtures/images/test_image.jpg new file mode 100644 index 0000000..2127b40 Binary files /dev/null and b/packages/operators/fixtures/images/test_image.jpg differ diff --git a/packages/operators/package.json b/packages/operators/package.json index bdb845f..4b310e3 100644 --- a/packages/operators/package.json +++ b/packages/operators/package.json @@ -20,5 +20,8 @@ "dependencies": { "@rxjs-collection/observables": "*", "rxjs": "7.8.1" + }, + "devDependencies": { + "node-fetch": "^3.3.2" } } diff --git a/packages/operators/src/fetch/autoPagination.js b/packages/operators/src/fetch/autoPagination.js new file mode 100644 index 0000000..d6ec57e --- /dev/null +++ b/packages/operators/src/fetch/autoPagination.js @@ -0,0 +1,21 @@ +import { concatMap, expand, filter, from, map } from 'rxjs'; + +import { download } from './download'; + +export const autoPagination = ({ resolveRoute }) => { + return source => + source.pipe( + concatMap(({ url }) => { + return from(resolveRoute(url)).pipe( + download(), + expand(resp => { + return from(resolveRoute(url, resp.clone())).pipe( + filter(url => !!url), + download() + ); + }) + ); + }), + map(resp => resp.clone()) + ); +}; diff --git a/packages/operators/src/fetch/autoPagination.test.js b/packages/operators/src/fetch/autoPagination.test.js new file mode 100644 index 0000000..5c36d15 --- /dev/null +++ b/packages/operators/src/fetch/autoPagination.test.js @@ -0,0 +1,42 @@ +import { concatAll, map, of } from 'rxjs'; +import { beforeEach, describe, expect, test } from 'vitest'; + +import { autoPagination } from './autoPagination'; +import { resolveJSON } from './resolve'; + +describe('auto pagination', function () { + beforeEach(function () { + // + }); + + test('auto pagination', async function () { + return new Promise(done => { + return of({ url: new URL('https://dummyjson.com/products') }) + .pipe( + autoPagination({ + resolveRoute: async (url, resp) => { + const data = (await resp?.json()) || { skip: -10, limit: 10 }; + + if (!data.total || data.total > data.skip + data.limit) { + const newUrl = new URL(`${url}`); + newUrl.searchParams.set('skip', data.skip + data.limit); + newUrl.searchParams.set('limit', data.limit); + newUrl.searchParams.set('select', 'title,price'); + return newUrl; + } + } + }), + resolveJSON(), + map(({ products }) => products), + concatAll() + ) + .subscribe({ + next: e => console.log(e), + complete: () => { + console.log('COMPLETE'); + done(); + } + }); + }); + }); +}); diff --git a/packages/operators/src/fetch/concurrentDownload.js b/packages/operators/src/fetch/concurrentDownload.js new file mode 100644 index 0000000..afcd4aa --- /dev/null +++ b/packages/operators/src/fetch/concurrentDownload.js @@ -0,0 +1,7 @@ +import { mergeMap, of } from 'rxjs'; + +import { download } from './download'; + +export const concurrentDownload = (concurrent = 1) => { + return source => source.pipe(mergeMap(url => of(url).pipe(download()), concurrent)); +}; diff --git a/packages/operators/src/fetch/concurrentDownload.test.js b/packages/operators/src/fetch/concurrentDownload.test.js new file mode 100644 index 0000000..3de0587 --- /dev/null +++ b/packages/operators/src/fetch/concurrentDownload.test.js @@ -0,0 +1,40 @@ +import { concatAll, map, of } from 'rxjs'; +import { beforeEach, describe, expect, test } from 'vitest'; + +import { concurrentDownload } from './concurrentDownload'; +import { resolveJSON } from './resolve'; + +describe('multi fetch', function () { + beforeEach(function () { + // + }); + + test('request pagination', async function () { + return new Promise(done => { + of( + new URL('https://dummyjson.com/products?limit=10&skip=0&select=title,price'), + new URL('https://dummyjson.com/products?limit=10&skip=10&select=title,price'), + new URL('https://dummyjson.com/products?limit=10&skip=20&select=title,price'), + new URL('https://dummyjson.com/products?limit=10&skip=30&select=title,price'), + new URL('https://dummyjson.com/products?limit=10&skip=40&select=title,price'), + new URL('https://dummyjson.com/products?limit=10&skip=50&select=title,price'), + new URL('https://dummyjson.com/products?limit=10&skip=60&select=title,price'), + new URL('https://dummyjson.com/products?limit=10&skip=70&select=title,price'), + new URL('https://dummyjson.com/products?limit=10&skip=80&select=title,price') + ) + .pipe( + concurrentDownload(4), + resolveJSON(), + map(({ products }) => products), + concatAll() + ) + .subscribe({ + next: e => console.log(e), + complete: () => { + console.log('COMPLETE'); + done(); + } + }); + }); + }); +}); diff --git a/packages/operators/src/fetch/download.js b/packages/operators/src/fetch/download.js new file mode 100644 index 0000000..929d7cb --- /dev/null +++ b/packages/operators/src/fetch/download.js @@ -0,0 +1,18 @@ +import { request } from './request'; +import { resolveBlob, resolveJSON, resolveText } from './resolve'; + +export const download = () => { + return source => source.pipe(request()); +}; + +export const downloadJSON = () => { + return source => source.pipe(download(), resolveJSON()); +}; + +export const downloadText = () => { + return source => source.pipe(download(), resolveText()); +}; + +export const downloadBlob = () => { + return source => source.pipe(download(), resolveBlob()); +}; diff --git a/packages/observables/src/fetch/request.test.js b/packages/operators/src/fetch/download.test.js similarity index 53% rename from packages/observables/src/fetch/request.test.js rename to packages/operators/src/fetch/download.test.js index ef16c3f..c11e1d2 100644 --- a/packages/observables/src/fetch/request.test.js +++ b/packages/operators/src/fetch/download.test.js @@ -1,9 +1,10 @@ import fetchMock from 'fetch-mock'; +import { of } from 'rxjs'; import { afterEach, test, describe, beforeEach, expect } from 'vitest'; -import { requestObservable } from './request.js'; +import { download, downloadJSON } from './download.js'; -describe('request observable with default operators', function () { +describe('download operator', function () { beforeEach(function () { fetchMock.get( 'https://httpbin.org/my-url-fast', @@ -23,12 +24,16 @@ describe('request observable with default operators', function () { fetchMock.restore(); }); - test('successfull request', () => + test('successfull download', () => new Promise(done => { - requestObservable('https://httpbin.org/my-url-fast').subscribe(async e => { - expect(e.ok).equal(true); - expect(await e.json()).deep.equal({ hello: 'fast world' }); - done(); - }); + of('https://httpbin.org/my-url-fast') + .pipe(downloadJSON()) + .subscribe({ + next: data => { + expect(data).deep.equal({ hello: 'fast world' }); + done(); + }, + complete: e => console.log('COMPLETE', e) + }); })); }); diff --git a/packages/operators/src/fetch/lazyPagination.js b/packages/operators/src/fetch/lazyPagination.js new file mode 100644 index 0000000..dae84cd --- /dev/null +++ b/packages/operators/src/fetch/lazyPagination.js @@ -0,0 +1,15 @@ +import { concatMap, map } from 'rxjs'; + +import { concurrentDownload } from './concurrentDownload'; + +export const lazyPagination = ({ createRoute }) => { + return source => + source.pipe( + concatMap(({ url, pager, concurrent }) => { + return pager.pipe( + map(options => createRoute(url, options)), + concurrentDownload(concurrent) + ); + }) + ); +}; diff --git a/packages/operators/src/fetch/lazyPagination.test.js b/packages/operators/src/fetch/lazyPagination.test.js new file mode 100644 index 0000000..3233f8f --- /dev/null +++ b/packages/operators/src/fetch/lazyPagination.test.js @@ -0,0 +1,50 @@ +import { concatAll, map, of, Subject } from 'rxjs'; +import { beforeEach, describe, expect, test } from 'vitest'; + +import { lazyPagination } from './lazyPagination'; +import { resolveJSON } from './resolve'; + +describe('lazy pagination operator', function () { + beforeEach(function () { + // + }); + + test('successfull lazy pagination', async function () { + const pager = new Subject(); + + return new Promise(done => { + of({ url: new URL('https://dummyjson.com/products'), pager, concurrent: 4 }) + .pipe( + lazyPagination({ + createRoute: (url, { value, limit = 10 }) => { + const newUrl = new URL(`${url}`); + newUrl.searchParams.set('skip', value * limit); + newUrl.searchParams.set('limit', limit); + newUrl.searchParams.set('select', 'title,price'); + return newUrl; + } + }), + resolveJSON(), + map(({ products }) => products), + concatAll() + ) + .subscribe({ + next: e => console.log(e), + complete: () => { + console.log('COMPLETE'); + done(); + } + }); + + pager.next({ value: 2 }); + pager.next({ value: 3 }); + pager.next({ value: 4 }); + pager.next({ value: 5 }); + pager.next({ value: 6 }); + pager.next({ value: 7 }); + pager.next({ value: 8 }); + pager.next({ value: 9 }); + pager.complete(); + }); + }); +}); diff --git a/packages/operators/src/fetch/polling.js b/packages/operators/src/fetch/polling.js new file mode 100644 index 0000000..737c501 --- /dev/null +++ b/packages/operators/src/fetch/polling.js @@ -0,0 +1,20 @@ +import { concatMap, delay as delayOperator, EMPTY, expand, of } from 'rxjs'; + +import { download } from './download'; + +export const polling = ({ validateResult, delay = 1000 }) => { + return source => + source.pipe( + concatMap(({ url }) => { + return of(url).pipe( + download(), + expand(data => { + if (validateResult(data)) { + return of(url).pipe(delayOperator(delay), download()); + } + return EMPTY; + }) + ); + }) + ); +}; diff --git a/packages/operators/src/fetch/polling.test.js b/packages/operators/src/fetch/polling.test.js new file mode 100644 index 0000000..6096170 --- /dev/null +++ b/packages/operators/src/fetch/polling.test.js @@ -0,0 +1,29 @@ +import { concatAll, map, of } from 'rxjs'; +import { beforeEach, describe, expect, test } from 'vitest'; + +import { polling } from './polling'; + +describe('polling', function () { + beforeEach(function () { + // + }); + + test('auto polling', async function () { + of({ url: new URL('https://dummyjson.com/products') }) + .pipe( + polling({ + validateResult: data => { + return data.total > data.skip + data.limit; + } + }) + // map(({ data: { products } }) => products), + // concatAll() + ) + .subscribe({ + next: e => console.log('aha'), + complete: () => console.log('COMPLETE') + }); + + await new Promise(resolve => setTimeout(resolve, 5000)); + }); +}); diff --git a/packages/operators/src/fetch/request.http b/packages/operators/src/fetch/request.http new file mode 100644 index 0000000..b6e83ff --- /dev/null +++ b/packages/operators/src/fetch/request.http @@ -0,0 +1,20 @@ +### Sample +# @name post +GET https://dummyjson.com/posts/1 HTTP/1.1 +content-type: application/json + +### Ref Sample +@userId = {{post.response.body.userId}} +GET https://dummyjson.com/users/{{userId}} HTTP/1.1 +content-type: application/json + +### Upload Sample +POST https://api.escuelajs.co/api/v1/files/upload HTTP/1.1 +Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gZ + +------WebKitFormBoundary7MA4YWxkTrZu0gZ +Content-Disposition: form-data; name="file"; filename="1.jpg" +Content-Type: image/jpeg + +< ../../fixtures/images/test_image.jpg +------WebKitFormBoundary7MA4YWxkTrZu0gZ-- diff --git a/packages/operators/src/fetch/request.js b/packages/operators/src/fetch/request.js new file mode 100644 index 0000000..e54f231 --- /dev/null +++ b/packages/operators/src/fetch/request.js @@ -0,0 +1,11 @@ +import { concatMap } from 'rxjs'; + +import { networkRetry } from './retry'; + +export const request = () => { + return source => + source.pipe( + concatMap(req => fetch(req)), + networkRetry() + ); +}; diff --git a/packages/operators/src/fetch/request.test.js b/packages/operators/src/fetch/request.test.js new file mode 100644 index 0000000..bdc667f --- /dev/null +++ b/packages/operators/src/fetch/request.test.js @@ -0,0 +1,37 @@ +import fetchMock from 'fetch-mock'; +import { of } from 'rxjs'; +import { afterEach, test, describe, beforeEach, expect } from 'vitest'; + +import { request } from './request.js'; + +describe('request observable with default operators', function () { + beforeEach(function () { + let counter = 0; + fetchMock.get( + 'https://httpbin.org/my-url-fast', + () => { + return new Response(JSON.stringify({ hello: 'fast world' }), { + status: ++counter > 2 ? 200 : 404, + headers: { 'Content-type': 'application/json' } + }); + }, + { delay: 0, repeat: 4 } + ); + }); + + afterEach(function () { + fetchMock.restore(); + }); + + test('successfull request', () => + new Promise(done => { + of('https://httpbin.org/my-url-fast') + .pipe(request()) + .subscribe({ + next: resp => { + expect(resp).deep.includes({ ok: true }); + }, + complete: () => done() + }); + })); +}); diff --git a/packages/operators/src/fetch/resolve.js b/packages/operators/src/fetch/resolve.js new file mode 100644 index 0000000..776fce2 --- /dev/null +++ b/packages/operators/src/fetch/resolve.js @@ -0,0 +1,17 @@ +import { concatMap } from 'rxjs'; + +export const resolve = (type = 'json') => { + return source => source.pipe(concatMap(e => e[String(type)]())); +}; + +export const resolveJSON = () => { + return resolve('json'); +}; + +export const resolveText = () => { + return resolve('text'); +}; + +export const resolveBlob = () => { + return resolve('blob'); +}; diff --git a/packages/operators/src/request/retry.js b/packages/operators/src/fetch/retry.js similarity index 96% rename from packages/operators/src/request/retry.js rename to packages/operators/src/fetch/retry.js index ee50e93..3d7666e 100644 --- a/packages/operators/src/request/retry.js +++ b/packages/operators/src/fetch/retry.js @@ -34,7 +34,7 @@ export const networkRetry = ({ timeout = defaultTimeout, count } = {}) => { tap(valid => (counter = counter * valid)), filter(valid => valid), tap(() => { - console.log(timeout(counter++)); + console.log(timeout(counter)); console.log( `retry: request - next: ${counter + 1} in ${timeout(counter + 1) || timeout}s` ); diff --git a/packages/operators/src/request/retry.test.js b/packages/operators/src/fetch/retry.test.js similarity index 79% rename from packages/operators/src/request/retry.test.js rename to packages/operators/src/fetch/retry.test.js index 3268ada..f02ff37 100644 --- a/packages/operators/src/request/retry.test.js +++ b/packages/operators/src/fetch/retry.test.js @@ -17,15 +17,7 @@ describe('request retry', function () { let counter = 0; const mockObservable = of(null) - .pipe( - map(() => { - counter++; - if (counter < 3) { - return { ok: false }; - } - return { ok: true }; - }) - ) + .pipe(map(() => ({ ok: !(++counter < 3) }))) .pipe(networkRetry({ timeout: () => 1000 })); testScheduler.run(({ expectObservable }) => { diff --git a/packages/operators/src/fetch/upload.js b/packages/operators/src/fetch/upload.js new file mode 100644 index 0000000..a7c09da --- /dev/null +++ b/packages/operators/src/fetch/upload.js @@ -0,0 +1,7 @@ +import { concatMap, of } from 'rxjs'; + +import { request } from './request'; + +export const upload = () => { + return source => source.pipe(request()); +}; diff --git a/packages/operators/src/fetch/upload.test.js b/packages/operators/src/fetch/upload.test.js new file mode 100644 index 0000000..c8f7028 --- /dev/null +++ b/packages/operators/src/fetch/upload.test.js @@ -0,0 +1,49 @@ +import fetchMock from 'fetch-mock'; +import { readFile } from 'node:fs/promises'; +import { of } from 'rxjs'; +import { afterEach, test, describe, beforeEach, expect } from 'vitest'; + +import { log } from '../log.js'; +import { resolveJSON } from './resolve.js'; +import { upload } from './upload.js'; + +describe('request observable with default operators', function () { + beforeEach(function () { + // + }); + + afterEach(function () { + // + }); + + test('successfull request', async () => { + const formData = new FormData(); + formData.set( + 'file', + new File( + [ + new Blob([await readFile('./packages/operators/fixtures/images/test_image.jpg')], { + type: 'image/jpeg' + }) + ], + 'test_image.jpg' + ) + ); + + const req = new Request(new URL('https://api.escuelajs.co/api/v1/files/upload'), { + method: 'POST', + body: formData + }); + + return new Promise(done => { + of(req) + .pipe(upload(), log(false), resolveJSON(), log(false)) + .subscribe(e => { + expect(e) + .deep.includes({ originalname: 'test_image.jpg' }) + .have.all.keys('filename', 'location'); + done(); + }); + }); + }); +}); diff --git a/packages/operators/src/log.js b/packages/operators/src/log.js new file mode 100644 index 0000000..535d267 --- /dev/null +++ b/packages/operators/src/log.js @@ -0,0 +1,26 @@ +import { Observable } from 'rxjs'; + +export const log = (active = true) => { + return source => { + if (active) { + return new Observable(observer => { + return source.subscribe( + val => { + console.log(val); + observer.next(val); + }, + err => { + console.error(err); + observer.error(err); + }, + () => { + console.log('%ccomplete', 'color: green'); + observer.complete(); + } + ); + }); + } else { + return source; + } + }; +}; diff --git a/packages/operators/vitest.config.js b/packages/operators/vitest.config.js index e3fd592..b34b1fe 100644 --- a/packages/operators/vitest.config.js +++ b/packages/operators/vitest.config.js @@ -2,6 +2,7 @@ import { defineProject } from 'vitest/config'; export default defineProject({ test: { + setupFiles: ['../../setup.js'], environment: 'happy-dom' } }); diff --git a/setup.js b/setup.js new file mode 100644 index 0000000..752a97b --- /dev/null +++ b/setup.js @@ -0,0 +1,17 @@ +import * as nFetch from 'node-fetch'; +import { afterAll, beforeAll } from 'vitest'; + +const backup = new Map([ + ['fetch', [global.fetch, nFetch.default]], + ['Blob', [global.Blob, nFetch.Blob]], + ['File', [global.File, nFetch.File]], + ['FormData', [global.FormData, nFetch.FormData]], + ['Request', [global.Request, nFetch.Request]] +]); + +beforeAll(() => { + Array.from(backup.entries()).forEach(([name, [, poly]]) => (global[String(name)] = poly)); +}); +afterAll(() => { + Array.from(backup.entries()).forEach(([name, [orig]]) => (global[String(name)] = orig)); +});