From de514595df437c8a38e39ef93f94cf3c1f453d4d Mon Sep 17 00:00:00 2001 From: printfn Date: Tue, 10 Dec 2024 08:55:21 +0000 Subject: [PATCH] Improve web worker robustness --- web/package-lock.json | 213 +++++++++++++++------------------- web/package.json | 4 +- web/src/App.tsx | 2 +- web/src/lib/WaitGroup.ts | 71 ++++++++++++ web/src/lib/exchange-rates.ts | 30 ++++- web/src/lib/fend.ts | 127 +++++++++++--------- 6 files changed, 264 insertions(+), 183 deletions(-) create mode 100644 web/src/lib/WaitGroup.ts diff --git a/web/package-lock.json b/web/package-lock.json index 1d74df18..c4341859 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -18,7 +18,7 @@ "@eslint/js": "^9.16.0", "@types/node": "^22.10.1", "@types/react": "^19.0.1", - "@types/react-dom": "^19.0.1", + "@types/react-dom": "^19.0.2", "@vitejs/plugin-react": "^4.3.4", "babel-plugin-react-compiler": "^19.0.0-beta-df7b47d-20241124", "eslint": "^9.16.0", @@ -28,7 +28,7 @@ "globals": "^15.13.0", "prettier": "^3.4.2", "typescript": "^5.7.2", - "typescript-eslint": "^8.17.0", + "typescript-eslint": "^8.18.0", "vite": "^6.0.3", "vite-plugin-wasm": "^3.3.0" } @@ -1060,18 +1060,14 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.7.tgz", + "integrity": "sha512-rNoqXKSm/P1EJw/lrzyaQepC6KfryLFSShA+jl1mbPYqutXlvZWwm0x03W6jMHaYJqIOmc7QarD+GVfG9hO7XQ==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/sourcemap-codec": "1.4.16-beta.0", "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" } }, "node_modules/@jridgewell/resolve-uri": { @@ -1084,20 +1080,10 @@ "node": ">=6.0.0" } }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "version": "1.4.16-beta.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.16-beta.0.tgz", + "integrity": "sha512-qiZJiTfyb00BApxRU7Apz/3jtlp4gKgOmCXlGQRlIQ5zg6U0uYIb8lZBfbiJ+TxAEJ+rczfY07+CExd8sTRo5w==", "dev": true, "license": "MIT" }, @@ -1112,6 +1098,13 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@jridgewell/trace-mapping/node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1496,27 +1489,27 @@ } }, "node_modules/@types/react-dom": { - "version": "19.0.1", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.0.1.tgz", - "integrity": "sha512-hljHij7MpWPKF6u5vojuyfV0YA4YURsQG7KT6SzV0Zs2BXAtgdTxG6A229Ub/xiWV4w/7JL8fi6aAyjshH4meA==", + "version": "19.0.2", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.0.2.tgz", + "integrity": "sha512-c1s+7TKFaDRRxr1TxccIX2u7sfCnc3RxkVyBIUA2lCpyqCF+QoAwQ/CBg7bsMdVwP120HEH143VQezKtef5nCg==", "dev": true, "license": "MIT", - "dependencies": { - "@types/react": "*" + "peerDependencies": { + "@types/react": "^19.0.0" } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.17.0.tgz", - "integrity": "sha512-HU1KAdW3Tt8zQkdvNoIijfWDMvdSweFYm4hWh+KwhPstv+sCmWb89hCIP8msFm9N1R/ooh9honpSuvqKWlYy3w==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.18.0.tgz", + "integrity": "sha512-NR2yS7qUqCL7AIxdJUQf2MKKNDVNaig/dEB0GBLU7D+ZdHgK1NoH/3wsgO3OnPVipn51tG3MAwaODEGil70WEw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.17.0", - "@typescript-eslint/type-utils": "8.17.0", - "@typescript-eslint/utils": "8.17.0", - "@typescript-eslint/visitor-keys": "8.17.0", + "@typescript-eslint/scope-manager": "8.18.0", + "@typescript-eslint/type-utils": "8.18.0", + "@typescript-eslint/utils": "8.18.0", + "@typescript-eslint/visitor-keys": "8.18.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -1531,25 +1524,21 @@ }, "peerDependencies": { "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", - "eslint": "^8.57.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.17.0.tgz", - "integrity": "sha512-Drp39TXuUlD49F7ilHHCG7TTg8IkA+hxCuULdmzWYICxGXvDXmDmWEjJYZQYgf6l/TFfYNE167m7isnc3xlIEg==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.18.0.tgz", + "integrity": "sha512-hgUZ3kTEpVzKaK3uNibExUYm6SKKOmTU2BOxBSvOYwtJEPdVQ70kZJpPjstlnhCHcuc2WGfSbpKlb/69ttyN5Q==", "dev": true, - "license": "BSD-2-Clause", + "license": "MITClause", "dependencies": { - "@typescript-eslint/scope-manager": "8.17.0", - "@typescript-eslint/types": "8.17.0", - "@typescript-eslint/typescript-estree": "8.17.0", - "@typescript-eslint/visitor-keys": "8.17.0", + "@typescript-eslint/scope-manager": "8.18.0", + "@typescript-eslint/types": "8.18.0", + "@typescript-eslint/typescript-estree": "8.18.0", + "@typescript-eslint/visitor-keys": "8.18.0", "debug": "^4.3.4" }, "engines": { @@ -1560,23 +1549,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.17.0.tgz", - "integrity": "sha512-/ewp4XjvnxaREtqsZjF4Mfn078RD/9GmiEAtTeLQ7yFdKnqwTOgRMSvFz4et9U5RiJQ15WTGXPLj89zGusvxBg==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.18.0.tgz", + "integrity": "sha512-PNGcHop0jkK2WVYGotk/hxj+UFLhXtGPiGtiaWgVBVP1jhMoMCHlTyJA+hEj4rszoSdLTK3fN4oOatrL0Cp+Xw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.17.0", - "@typescript-eslint/visitor-keys": "8.17.0" + "@typescript-eslint/types": "8.18.0", + "@typescript-eslint/visitor-keys": "8.18.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1587,14 +1572,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.17.0.tgz", - "integrity": "sha512-q38llWJYPd63rRnJ6wY/ZQqIzPrBCkPdpIsaCfkR3Q4t3p6sb422zougfad4TFW9+ElIFLVDzWGiGAfbb/v2qw==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.18.0.tgz", + "integrity": "sha512-er224jRepVAVLnMF2Q7MZJCq5CsdH2oqjP4dT7K6ij09Kyd+R21r7UVJrF0buMVdZS5QRhDzpvzAxHxabQadow==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.17.0", - "@typescript-eslint/utils": "8.17.0", + "@typescript-eslint/typescript-estree": "8.18.0", + "@typescript-eslint/utils": "8.18.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -1606,18 +1591,14 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.17.0.tgz", - "integrity": "sha512-gY2TVzeve3z6crqh2Ic7Cr+CAv6pfb0Egee7J5UAVWCpVvDI/F71wNfolIim4FE6hT15EbpZFVUj9j5i38jYXA==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.18.0.tgz", + "integrity": "sha512-FNYxgyTCAnFwTrzpBGq+zrnoTO4x0c1CKYY5MuUTzpScqmY5fmsh2o3+57lqdI3NZucBDCzDgdEbIaNfAjAHQA==", "dev": true, "license": "MIT", "engines": { @@ -1629,14 +1610,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.17.0.tgz", - "integrity": "sha512-JqkOopc1nRKZpX+opvKqnM3XUlM7LpFMD0lYxTqOTKQfCWAmxw45e3qlOCsEqEB2yuacujivudOFpCnqkBDNMw==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.18.0.tgz", + "integrity": "sha512-rqQgFRu6yPkauz+ms3nQpohwejS8bvgbPyIDq13cgEDbkXt4LH4OkDMT0/fN1RUtzG8e8AKJyDBoocuQh8qNeg==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.17.0", - "@typescript-eslint/visitor-keys": "8.17.0", + "@typescript-eslint/types": "8.18.0", + "@typescript-eslint/visitor-keys": "8.18.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -1651,10 +1632,8 @@ "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "peerDependencies": { + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { @@ -1697,16 +1676,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.17.0.tgz", - "integrity": "sha512-bQC8BnEkxqG8HBGKwG9wXlZqg37RKSMY7v/X8VEWD8JG2JuTHuNK0VFvMPMUKQcbk6B+tf05k+4AShAEtCtJ/w==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.18.0.tgz", + "integrity": "sha512-p6GLdY383i7h5b0Qrfbix3Vc3+J2k6QWw6UMUeY5JGfm3C5LbZ4QIZzJNoNOfgyRe0uuYKjvVOsO/jD4SJO+xg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.17.0", - "@typescript-eslint/types": "8.17.0", - "@typescript-eslint/typescript-estree": "8.17.0" + "@typescript-eslint/scope-manager": "8.18.0", + "@typescript-eslint/types": "8.18.0", + "@typescript-eslint/typescript-estree": "8.18.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1716,22 +1695,18 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.17.0.tgz", - "integrity": "sha512-1Hm7THLpO6ww5QU6H/Qp+AusUUl+z/CAm3cNZZ0jQvon9yicgO7Rwd+/WWRpMKLYV6p2UvdbR27c86rzCPpreg==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.18.0.tgz", + "integrity": "sha512-pCh/qEA8Lb1wVIqNvBke8UaRjJ6wrAWkJO5yyIbs8Yx6TNGYyfNjOo61tLv+WwLvoLPp4BQ8B7AHKijl8NGUfw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.17.0", + "@typescript-eslint/types": "8.18.0", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -2029,9 +2004,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.71", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.71.tgz", - "integrity": "sha512-dB68l59BI75W1BUGVTAEJy45CEVuEGy9qPVVQ8pnHyHMn36PLPPoE1mjLH+lo9rKulO3HC2OhbACI/8tCqJBcA==", + "version": "1.5.72", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.72.tgz", + "integrity": "sha512-ZpSAUOZ2Izby7qnZluSrAlGgGQzucmFbN0n64dYzocYxnxV5ufurpj3VgEe4cUp7ir9LmeLxNYo8bVnlM8bQHw==", "dev": true, "license": "ISC" }, @@ -2782,9 +2757,9 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", - "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", "dev": true, "license": "MIT" }, @@ -3231,15 +3206,15 @@ } }, "node_modules/typescript-eslint": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.17.0.tgz", - "integrity": "sha512-409VXvFd/f1br1DCbuKNFqQpXICoTB+V51afcwG1pn1a3Cp92MqAUges3YjwEdQ0cMUoCIodjVDAYzyD8h3SYA==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.18.0.tgz", + "integrity": "sha512-Xq2rRjn6tzVpAyHr3+nmSg1/9k9aIHnJ2iZeOH7cfGOWqTkXTm3kwpQglEuLGdNrYvPF+2gtAs+/KF5rjVo+WQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.17.0", - "@typescript-eslint/parser": "8.17.0", - "@typescript-eslint/utils": "8.17.0" + "@typescript-eslint/eslint-plugin": "8.18.0", + "@typescript-eslint/parser": "8.18.0", + "@typescript-eslint/utils": "8.18.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3249,12 +3224,8 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/undici-types": { @@ -3434,9 +3405,9 @@ } }, "node_modules/zod": { - "version": "3.23.8", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", - "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "version": "3.24.0", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.0.tgz", + "integrity": "sha512-Hz+wiY8yD0VLA2k/+nsg2Abez674dDGTai33SwNvMPuf9uIrBC9eFgIMQxBBbHFxVXi8W+5nX9DcAh9YNSQm/w==", "dev": true, "license": "MIT", "funding": { diff --git a/web/package.json b/web/package.json index 079e01ff..caabf979 100644 --- a/web/package.json +++ b/web/package.json @@ -21,7 +21,7 @@ "@eslint/js": "^9.16.0", "@types/node": "^22.10.1", "@types/react": "^19.0.1", - "@types/react-dom": "^19.0.1", + "@types/react-dom": "^19.0.2", "@vitejs/plugin-react": "^4.3.4", "babel-plugin-react-compiler": "^19.0.0-beta-df7b47d-20241124", "eslint": "^9.16.0", @@ -31,7 +31,7 @@ "globals": "^15.13.0", "prettier": "^3.4.2", "typescript": "^5.7.2", - "typescript-eslint": "^8.17.0", + "typescript-eslint": "^8.18.0", "vite": "^6.0.3", "vite-plugin-wasm": "^3.3.0" }, diff --git a/web/src/App.tsx b/web/src/App.tsx index c3cc56a7..35588712 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -91,7 +91,7 @@ export default function App({ widget = false }: { widget?: boolean }) { return; } onInput(''); - const result =

{fendResult.ok ? fendResult.result : fendResult.message}

; + const result =

{fendResult.ok ? fendResult.result : `Error: ${fendResult.message}`}

; setOutput(o => ( <> {o} diff --git a/web/src/lib/WaitGroup.ts b/web/src/lib/WaitGroup.ts new file mode 100644 index 00000000..a1376679 --- /dev/null +++ b/web/src/lib/WaitGroup.ts @@ -0,0 +1,71 @@ +export class WaitGroup { + #counter = 0; + #promise = Promise.resolve(); + #resolve?: () => void; + + enter() { + if (this.#counter === 0) { + this.#promise = new Promise(resolve => { + this.#resolve = resolve; + }); + } + ++this.#counter; + } + leave() { + if (this.#counter <= 0 || !this.#resolve) { + throw new Error('leave() called without a matching enter()'); + } + --this.#counter; + if (this.#counter === 0) { + this.#resolve(); + this.#resolve = undefined; + } + } + async wait(abortSignal?: AbortSignal) { + if (abortSignal) { + await abortPromise(abortSignal, async () => { + await this.#promise; + }); + } else { + await this.#promise; + } + } + + get counter() { + return this.#counter; + } +} + +async function abortPromise(abortSignal: AbortSignal, f: () => Promise) { + return new Promise((resolve, reject) => { + if (abortSignal.aborted) { + // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors + reject(abortSignal.reason); + return; + } + + const onAbort = () => { + cleanup(); + // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors + reject(abortSignal.reason); + }; + + const cleanup = () => { + abortSignal.removeEventListener('abort', onAbort); + }; + + abortSignal.addEventListener('abort', onAbort); + + f().then( + value => { + cleanup(); + resolve(value); + }, + (error: unknown) => { + cleanup(); + // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors + reject(error); + }, + ); + }); +} diff --git a/web/src/lib/exchange-rates.ts b/web/src/lib/exchange-rates.ts index 39588271..2c91938d 100644 --- a/web/src/lib/exchange-rates.ts +++ b/web/src/lib/exchange-rates.ts @@ -1,7 +1,11 @@ -export async function getExchangeRates() { - const map = new Map(); +import { WaitGroup } from './WaitGroup'; +const wg = new WaitGroup(); +let exchangeRateCache: Map | undefined; + +async function fetchExchangeRates() { try { + const map = new Map(); const res = await fetch('https://fend.pr.workers.dev/exchange-rates'); const xml = await res.text(); const dom = new DOMParser().parseFromString(xml, 'text/xml'); @@ -17,9 +21,27 @@ export async function getExchangeRates() { map.set(currency, rate); } } + return map; } catch (e) { - console.error('Failed to fetch currencies', e); + throw new Error('failed to fetch currencies', { cause: e }); } +} - return map; +export async function getExchangeRates(): Promise> { + await wg.wait(); + + try { + wg.enter(); + if (exchangeRateCache) { + return exchangeRateCache; + } + exchangeRateCache = await fetchExchangeRates(); + return exchangeRateCache; + } catch (e) { + console.log(e); + exchangeRateCache = new Map(); + return exchangeRateCache; + } finally { + wg.leave(); + } } diff --git a/web/src/lib/fend.ts b/web/src/lib/fend.ts index 24d47b90..1579240e 100644 --- a/web/src/lib/fend.ts +++ b/web/src/lib/fend.ts @@ -2,78 +2,95 @@ import { getExchangeRates } from './exchange-rates'; import type { FendArgs, FendResult } from './worker'; import MyWorker from './worker?worker'; -let exchangeRateCache: Map | null = null; +function newAbortError(message: string) { + const e = new Error(message); + e.name = 'AbortError'; + return e; +} + type State = 'new' | 'ready' | 'busy'; -class WorkerCache { - worker!: Worker; - state!: State; - initialisedPromise!: Promise; +type WorkerCache = { + worker: Worker; + state: State; + initialisedPromise: Promise; resolveDone?: (r: FendResult) => void; rejectError?: (e: Error) => void; +}; - init() { - this.state = 'new'; - this.worker = new MyWorker({ +function init() { + let resolveInitialised: () => void; + const result: WorkerCache = { + state: 'new', + worker: new MyWorker({ name: 'fend worker', - }); - let resolveInitialised: () => void; - this.initialisedPromise = new Promise(resolve => { + }), + initialisedPromise: new Promise(resolve => { resolveInitialised = resolve; - }); - this.worker.onmessage = (e: MessageEvent) => { - this.state = 'ready'; - if (e.data === 'ready') { - resolveInitialised(); - } else { - this.resolveDone?.(e.data); - } - }; - this.worker.onerror = e => { - this.state = 'ready'; - this.rejectError?.(new Error(e.message, { cause: e })); - }; - this.worker.onmessageerror = e => { - this.state = 'ready'; - this.rejectError?.(new Error('received messageerror event', { cause: e })); - }; - } + }), + }; + result.worker.onmessage = (e: MessageEvent) => { + result.state = 'ready'; + if (e.data === 'ready') { + resolveInitialised(); + } else { + result.resolveDone?.(e.data); + } + }; + result.worker.onerror = e => { + result.state = 'ready'; + result.rejectError?.(new Error(e.message, { cause: e })); + }; + result.worker.onmessageerror = e => { + result.state = 'ready'; + result.rejectError?.(new Error('received messageerror event', { cause: e })); + }; + return result; +} - constructor() { - this.init(); - } +let workerCache: WorkerCache = init(); +let id = 0; - async query(args: FendArgs) { - if (this.state === 'new') { - await this.initialisedPromise; - } - if (this.state === 'busy') { - console.log('terminating existing worker'); - this.worker.terminate(); - this.resolveDone?.({ ok: false, message: 'cancelled' }); - this.init(); - await this.initialisedPromise; +async function query(args: FendArgs) { + let w = workerCache; + const i = ++id; + if (w.state === 'new') { + await w.initialisedPromise; + if (i < id) { + throw newAbortError('created new worker during initialisation'); } - if (this.state !== 'ready') { - throw new Error('unexpected worker state: ' + this.state); + } + if (w.state === 'busy') { + console.log('terminating existing worker'); + w.worker.terminate(); + w.resolveDone?.({ ok: false, message: 'cancelled' }); + w = init(); + workerCache = w; + await w.initialisedPromise; + if (i < id) { + throw newAbortError('created new worker while worker was busy'); } - const p = new Promise((resolve, reject) => { - this.resolveDone = resolve; - this.rejectError = reject; - }); - this.state = 'busy'; - this.worker.postMessage(args); - return await p; } + if (w.state !== 'ready') { + throw new Error('unexpected worker state: ' + w.state); + } + const p = new Promise((resolve, reject) => { + w.resolveDone = resolve; + w.rejectError = reject; + }); + w.state = 'busy'; + w.worker.postMessage(args); + return await p; } -const workerCache = new WorkerCache(); export async function fend(input: string, timeout: number, variables: string): Promise { try { - const currencyData = exchangeRateCache || (await getExchangeRates()); - exchangeRateCache = currencyData; + const currencyData = await getExchangeRates(); const args: FendArgs = { input, timeout, variables, currencyData }; - return await workerCache.query(args); + return await query(args); } catch (e) { + if (e instanceof Error && e.name === 'AbortError') { + return { ok: false, message: 'Aborted' }; + } console.error(e); alert('Failed to initialise WebAssembly'); return { ok: false, message: 'Failed to initialise WebAssembly' };