diff --git a/.github/workflows/build-and-lint.yml b/.github/workflows/build-and-lint.yml index 221a2c1..8c15e81 100644 --- a/.github/workflows/build-and-lint.yml +++ b/.github/workflows/build-and-lint.yml @@ -92,3 +92,47 @@ jobs: - name: Build run: npm run build + env: + NEXT_PUBLIC_HASURA_GRAPHQL_URL: "https://unreachable.invalid/v1/graphql" + NEXT_PUBLIC_POLL_INTERVAL: "30000" + NEXT_PUBLIC_HASURA_GRAPHQL_JWT_TOKEN: "dummy-jwt-token" + + - name: Test + run: npm test + + - name: Smoke test - server renders dynamic routes + env: + NEXT_PUBLIC_HASURA_GRAPHQL_URL: "https://unreachable.invalid/v1/graphql" + NEXT_PUBLIC_POLL_INTERVAL: "30000" + NEXT_PUBLIC_HASURA_GRAPHQL_JWT_TOKEN: "dummy-jwt-token" + run: | + npm start & + SERVER_PID=$! + + echo "Waiting for server to become ready..." + READY=false + for i in $(seq 1 30); do + if curl -s -o /dev/null "http://localhost:3000/"; then + READY=true + break + fi + sleep 1 + done + + if [ "$READY" != "true" ]; then + echo "Server never became ready within timeout" + kill "$SERVER_PID" 2>/dev/null || true + exit 1 + fi + + ROOT_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:3000/") + REQUEST_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:3000/request/0x1234567890abcdef1234567890abcdef12345678-0") + + echo "Observed status codes: / -> $ROOT_STATUS, /request/ -> $REQUEST_STATUS" + + kill "$SERVER_PID" 2>/dev/null || true + + if [ "$ROOT_STATUS" -ge 500 ] || [ "$REQUEST_STATUS" -ge 500 ]; then + echo "Smoke test failed: server returned a 5xx status code" + exit 1 + fi diff --git a/package-lock.json b/package-lock.json index 2a3f4a0..089e769 100644 --- a/package-lock.json +++ b/package-lock.json @@ -49,7 +49,8 @@ "lint-staged": "^15.2.10", "postcss": "^8", "tailwindcss": "^3.4.1", - "typescript": "^5" + "typescript": "^5", + "vitest": "^4.1.10" } }, "node_modules/@adraffy/ens-normalize": { @@ -234,6 +235,43 @@ "node": ">=14.21.3" } }, + "node_modules/@emnapi/core": { + "version": "2.0.0-alpha.3", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-2.0.0-alpha.3.tgz", + "integrity": "sha512-AZypUeJ/yByuxyS7BlSNRDOMLMlROYtjYdIAuBmJssVz1UJDSeYxLrdizhXCFYhedC5bqd/ASy8EuNXbVVXp9g==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@emnapi/wasi-threads": "2.0.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "2.0.0-alpha.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-2.0.0-alpha.3.tgz", + "integrity": "sha512-hFPAhMUjJD9BSyCANEISPOogeXC9Zo9ZQl7L6vKnaVsMkCtzznaW/naYypeyl0Gv5rYfWYsZbpixTMpjDJzQeA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-2.0.1.tgz", + "integrity": "sha512-9DsSk+o5NBX0CCJT8s0EROGSGxjR/tKu6aBTaVyq+SjAEQH4XcdcRxPBRzsBLizTTJ49MJjF+jgu3qnO9GLQcQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -1130,9 +1168,10 @@ } }, "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.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", @@ -1224,6 +1263,28 @@ "ts-toolbelt": "^9.6.0" } }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.0.tgz", + "integrity": "sha512-kDoONqMa+VnZ4vvvu/ZUurpJ4gkZU57e7g69qpNgWhYcZFPUHZM2CEMKm+cG6ufDVALbjMvfmMjFVqaK7uEMnA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^2.0.0-alpha.3", + "@emnapi/runtime": "^2.0.0-alpha.3" + } + }, "node_modules/@next/env": { "version": "14.2.35", "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.35.tgz", @@ -1458,6 +1519,16 @@ "node": ">=12.4.0" } }, + "node_modules/@oxc-project/types": { + "version": "0.139.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz", + "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -2281,6 +2352,304 @@ "node": ">=18.0.0" } }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", + "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", + "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", + "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", + "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", + "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", + "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", + "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", + "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", + "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", + "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", + "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", + "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", + "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", + "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", + "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -2337,6 +2706,13 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, "node_modules/@swc/counter": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", @@ -2409,6 +2785,42 @@ "npm": ">=9.x" } }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/file-saver": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.7.tgz", @@ -2595,6 +3007,92 @@ "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", "dev": true }, + "node_modules/@vitest/expect": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", + "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", + "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", + "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.10", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", + "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "@vitest/utils": "4.1.10", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", + "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", + "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/abitype": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.0.5.tgz", @@ -2910,6 +3408,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/ast-types-flow": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", @@ -3216,6 +3724,16 @@ "resolved": "https://registry.npmjs.org/cbor-js/-/cbor-js-0.1.0.tgz", "integrity": "sha512-7sQ/TvDZPl7csT1Sif9G0+MA0I0JOVah8+wWlJVQdVEgIbCzlN/ab3x+uvMNsc34TUvO6osQTAmB2ls80JX6tw==" }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -3420,6 +3938,13 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/core-js": { "version": "3.40.0", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.40.0.tgz", @@ -3642,6 +4167,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/detect-node-es": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", @@ -3871,6 +4406,13 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "dev": true, + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", @@ -4361,6 +4903,16 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -4446,6 +4998,16 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -5777,35 +6339,296 @@ "json-buffer": "3.0.1" } }, - "node_modules/language-subtag-registry": { - "version": "0.3.23", - "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", - "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", - "dev": true - }, - "node_modules/language-tags": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", - "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", + "node_modules/language-subtag-registry": { + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", + "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", + "dev": true + }, + "node_modules/language-tags": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", + "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", + "dev": true, + "dependencies": { + "language-subtag-registry": "^0.3.20" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], "dev": true, - "dependencies": { - "language-subtag-registry": "^0.3.20" - }, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=0.10" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">= 0.8.0" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, "node_modules/lilconfig": { @@ -6167,6 +6990,16 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" } }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -6310,15 +7143,16 @@ } }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -6644,6 +7478,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/obug": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -6800,6 +7648,13 @@ "node": ">=8" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", @@ -6808,9 +7663,10 @@ "optional": true }, "node_modules/picocolors": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", - "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==" + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" }, "node_modules/picomatch": { "version": "2.3.1", @@ -6861,9 +7717,9 @@ } }, "node_modules/postcss": { - "version": "8.4.45", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.45.tgz", - "integrity": "sha512-7KTLTdzdZZYscUc65XmjFiB73vBhBfbPztCYdUNvlaso9PrzjzcmjqBPR0lNGkcVlcO4BjiO5rK/qNz+XAen1Q==", + "version": "8.5.24", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.24.tgz", + "integrity": "sha512-8RyVklq0owXUTa4xlpzu4l9AaVKIdQvAcOHZWaMh98HgySsUtxRVf/chRe3dsSLqb6i40BzGRzEUddRaI+9TSw==", "funding": [ { "type": "opencollective", @@ -6878,10 +7734,11 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "nanoid": "^3.3.7", - "picocolors": "^1.0.1", - "source-map-js": "^1.2.0" + "nanoid": "^3.3.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12 || >=14" @@ -7407,6 +8264,40 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rolldown": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", + "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.139.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.1.5", + "@rolldown/binding-darwin-arm64": "1.1.5", + "@rolldown/binding-darwin-x64": "1.1.5", + "@rolldown/binding-freebsd-x64": "1.1.5", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", + "@rolldown/binding-linux-arm64-gnu": "1.1.5", + "@rolldown/binding-linux-arm64-musl": "1.1.5", + "@rolldown/binding-linux-ppc64-gnu": "1.1.5", + "@rolldown/binding-linux-s390x-gnu": "1.1.5", + "@rolldown/binding-linux-x64-gnu": "1.1.5", + "@rolldown/binding-linux-x64-musl": "1.1.5", + "@rolldown/binding-openharmony-arm64": "1.1.5", + "@rolldown/binding-wasm32-wasi": "1.1.5", + "@rolldown/binding-win32-arm64-msvc": "1.1.5", + "@rolldown/binding-win32-x64-msvc": "1.1.5" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -7602,6 +8493,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -7671,9 +8569,10 @@ } }, "node_modules/source-map-js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", - "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -7687,6 +8586,13 @@ "source-map": "^0.6.0" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, "node_modules/stackblur-canvas": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz", @@ -7697,6 +8603,13 @@ "node": ">=0.1.14" } }, + "node_modules/std-env": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "dev": true, + "license": "MIT" + }, "node_modules/stop-iteration-iterator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", @@ -8196,6 +9109,81 @@ "resolved": "https://registry.npmjs.org/timeago.js/-/timeago.js-4.0.2.tgz", "integrity": "sha512-a7wPxPdVlQL7lqvitHGGRsofhdwtkoSXPGATFuSOA2i1ZNQEPLrGnj68vOp2sOJTCFAQVXPeNMX/GctBaO9L2w==" }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz", + "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -8508,6 +9496,214 @@ } } }, + "node_modules/vitest": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", + "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.10", + "@vitest/mocker": "4.1.10", + "@vitest/pretty-format": "4.1.10", + "@vitest/runner": "4.1.10", + "@vitest/snapshot": "4.1.10", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.10", + "@vitest/browser-preview": "4.1.10", + "@vitest/browser-webdriverio": "4.1.10", + "@vitest/coverage-istanbul": "4.1.10", + "@vitest/coverage-v8": "4.1.10", + "@vitest/ui": "4.1.10", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/node_modules/@vitest/mocker": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", + "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.10", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest/node_modules/vite": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz", + "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.17", + "rolldown": "~1.1.5", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.3.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, "node_modules/webauthn-p256": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/webauthn-p256/-/webauthn-p256-0.0.5.tgz", @@ -8630,6 +9826,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", diff --git a/package.json b/package.json index bb49f25..9f4e814 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "format": "biome format --write", "check": "biome check --write", "lint-staged": "lint-staged", - "prepare": "husky install" + "prepare": "husky install", + "test": "vitest run" }, "dependencies": { "@next/third-parties": "^15.0.3", @@ -54,6 +55,7 @@ "lint-staged": "^15.2.10", "postcss": "^8", "tailwindcss": "^3.4.1", - "typescript": "^5" + "typescript": "^5", + "vitest": "^4.1.10" } } diff --git a/src/lib/consts.ts b/src/lib/consts.ts index 9d54e84..ba01433 100644 --- a/src/lib/consts.ts +++ b/src/lib/consts.ts @@ -69,6 +69,35 @@ export enum PAYMENT_CHAINS { ZKSYNCERA = "payment_zksyncera", } +/** + * Every payment subgraph remote, used to fan out the payment queries one + * request per chain. + */ +export const PAYMENT_CHAIN_REMOTES: readonly PAYMENT_CHAINS[] = + Object.values(PAYMENT_CHAINS); + +/** + * Remotes that expose the `singleRequestProxyDeployments` entity. + * + * This is intentionally NOT `PAYMENT_CHAIN_REMOTES`: the Single Request + * Forwarder subgraphs are only deployed on a subset of chains, and + * `payment_zksyncera` in particular has no `singleRequestProxyDeployments` + * field at all, so querying it always fails with + * `field 'singleRequestProxyDeployments' not found in type: 'payment_zksynceraQuery'`. + */ +export const SRF_CHAIN_REMOTES: readonly PAYMENT_CHAINS[] = [ + PAYMENT_CHAINS.MAINNET, + PAYMENT_CHAINS.ARBITRUM_ONE, + PAYMENT_CHAINS.AVALANCHE, + PAYMENT_CHAINS.BASE, + PAYMENT_CHAINS.BSC, + PAYMENT_CHAINS.CELO, + PAYMENT_CHAINS.MATIC, + PAYMENT_CHAINS.OPTIMISM, + PAYMENT_CHAINS.SEPOLIA, + PAYMENT_CHAINS.XDAI, +]; + export const PUBLIC_CLIENTS = { [CHAINS.MAINNET]: createPublicClient({ chain: mainnet, diff --git a/src/lib/hooks/payments.ts b/src/lib/hooks/payments.ts index 5f2e0c0..dfba6dc 100644 --- a/src/lib/hooks/payments.ts +++ b/src/lib/hooks/payments.ts @@ -1,169 +1,12 @@ /** @format */ -import { gql } from "graphql-request"; -import { graphQLClient } from "../graphQlClient"; -import { CORE_PAYMENT_FIELDS } from "../queries/utils"; -import type { Payment } from "../types"; -import { formatPaymentData } from "../utils"; - -export const PAYMENTS_QUERY = gql` - ${CORE_PAYMENT_FIELDS} - query PaymentsQuery($first: Int, $skip: Int!) { - # - payment_mainnet { - payments( - first: $first - skip: $skip - orderBy: timestamp - orderDirection: desc - ) { - ...PaymentFields - } - } - payment_arbitrum_one { - payments( - first: $first - skip: $skip - orderBy: timestamp - orderDirection: desc - ) { - ...PaymentFields - } - } - payment_avalanche { - payments( - first: $first - skip: $skip - orderBy: timestamp - orderDirection: desc - ) { - ...PaymentFields - } - } - payment_base { - payments( - first: $first - skip: $skip - orderBy: timestamp - orderDirection: desc - ) { - ...PaymentFields - } - } - payment_bsc { - payments( - first: $first - skip: $skip - orderBy: timestamp - orderDirection: desc - ) { - ...PaymentFields - } - } - payment_celo { - payments( - first: $first - skip: $skip - orderBy: timestamp - orderDirection: desc - ) { - ...PaymentFields - } - } - payment_fantom { - payments( - first: $first - skip: $skip - orderBy: timestamp - orderDirection: desc - ) { - ...PaymentFields - } - } - payment_fuse { - payments( - first: $first - skip: $skip - orderBy: timestamp - orderDirection: desc - ) { - ...PaymentFields - } - } - payment_matic { - payments( - first: $first - skip: $skip - orderBy: timestamp - orderDirection: desc - ) { - ...PaymentFields - } - } - payment_moonbeam { - payments( - first: $first - skip: $skip - orderBy: timestamp - orderDirection: desc - ) { - ...PaymentFields - } - } - payment_optimism { - payments( - first: $first - skip: $skip - orderBy: timestamp - orderDirection: desc - ) { - ...PaymentFields - } - } - payment_sepolia { - payments( - first: $first - skip: $skip - orderBy: timestamp - orderDirection: desc - ) { - ...PaymentFields - } - } - payment_xdai { - payments( - first: $first - skip: $skip - orderBy: timestamp - orderDirection: desc - ) { - ...PaymentFields - } - } - payment_zksyncera { - payments( - first: $first - skip: $skip - orderBy: timestamp - orderDirection: desc - ) { - ...PaymentFields - } - } - } -`; - -export const fetchPayments = async (variables: { - first: number; - skip: number; -}): Promise => { - try { - const data: { [x: string]: { payments: Payment[] } } = - await graphQLClient.request(PAYMENTS_QUERY, variables); - - return formatPaymentData(data); - } catch (err) { - console.error("Error fetching payments", err); - return []; - } -}; +/** + * This module used to hold a byte-for-byte duplicate of the payments query and + * a `fetchPayments` that swallowed every error and returned `[]`, which made a + * failed request indistinguishable from "no payments" at the query layer. + * + * It now re-exports the single implementation in `../queries/payments`, which + * degrades to partial data when individual chains fail and propagates the error + * when the whole request fails, so react-query reports `status === "error"`. + */ +export { buildPaymentsQuery, fetchPayments } from "../queries/payments"; diff --git a/src/lib/queries/address-payments.ts b/src/lib/queries/address-payments.ts index c21894c..40dae8a 100644 --- a/src/lib/queries/address-payments.ts +++ b/src/lib/queries/address-payments.ts @@ -1,158 +1,15 @@ /** @format */ import { gql } from "graphql-request"; -import { graphQLClient } from "../graphQlClient"; +import { PAYMENT_CHAIN_REMOTES } from "../consts"; import type { Payment } from "../types"; import { formatPaymentData } from "../utils"; -import { CORE_PAYMENT_FIELDS } from "./utils"; +import { CORE_PAYMENT_FIELDS, requestPerChain } from "./utils"; -export const ADDRESS_PAYMENTS_QUERY = gql` +export const buildAddressPaymentsQuery = (chain: string) => gql` ${CORE_PAYMENT_FIELDS} query AddressPaymentsQuery($first: Int, $skip: Int!, $address: Bytes) { - payment_mainnet { - payments( - first: $first - skip: $skip - orderBy: timestamp - orderDirection: desc - where: { or: [{ to: $address }, { from: $address }] } - ) { - ...PaymentFields - } - } - payment_arbitrum_one { - payments( - first: $first - skip: $skip - orderBy: timestamp - orderDirection: desc - where: { or: [{ to: $address }, { from: $address }] } - ) { - ...PaymentFields - } - } - payment_avalanche { - payments( - first: $first - skip: $skip - orderBy: timestamp - orderDirection: desc - where: { or: [{ to: $address }, { from: $address }] } - ) { - ...PaymentFields - } - } - payment_base { - payments( - first: $first - skip: $skip - orderBy: timestamp - orderDirection: desc - where: { or: [{ to: $address }, { from: $address }] } - ) { - ...PaymentFields - } - } - payment_bsc { - payments( - first: $first - skip: $skip - orderBy: timestamp - orderDirection: desc - where: { or: [{ to: $address }, { from: $address }] } - ) { - ...PaymentFields - } - } - payment_celo { - payments( - first: $first - skip: $skip - orderBy: timestamp - orderDirection: desc - where: { or: [{ to: $address }, { from: $address }] } - ) { - ...PaymentFields - } - } - payment_fantom { - payments( - first: $first - skip: $skip - orderBy: timestamp - orderDirection: desc - where: { or: [{ to: $address }, { from: $address }] } - ) { - ...PaymentFields - } - } - payment_fuse { - payments( - first: $first - skip: $skip - orderBy: timestamp - orderDirection: desc - where: { or: [{ to: $address }, { from: $address }] } - ) { - ...PaymentFields - } - } - payment_matic { - payments( - first: $first - skip: $skip - orderBy: timestamp - orderDirection: desc - where: { or: [{ to: $address }, { from: $address }] } - ) { - ...PaymentFields - } - } - payment_moonbeam { - payments( - first: $first - skip: $skip - orderBy: timestamp - orderDirection: desc - where: { or: [{ to: $address }, { from: $address }] } - ) { - ...PaymentFields - } - } - payment_optimism { - payments( - first: $first - skip: $skip - orderBy: timestamp - orderDirection: desc - where: { or: [{ to: $address }, { from: $address }] } - ) { - ...PaymentFields - } - } - payment_sepolia { - payments( - first: $first - skip: $skip - orderBy: timestamp - orderDirection: desc - where: { or: [{ to: $address }, { from: $address }] } - ) { - ...PaymentFields - } - } - payment_xdai { - payments( - first: $first - skip: $skip - orderBy: timestamp - orderDirection: desc - where: { or: [{ to: $address }, { from: $address }] } - ) { - ...PaymentFields - } - } - payment_zksyncera { + ${chain} { payments( first: $first skip: $skip @@ -172,8 +29,12 @@ export const fetchAddressPayments = async (variables: { address: string; }): Promise => { try { - const data: { [x: string]: { payments: Payment[] } } = - await graphQLClient.request(ADDRESS_PAYMENTS_QUERY, variables); + const data = await requestPerChain<{ payments: Payment[] }>( + "fetchAddressPayments", + PAYMENT_CHAIN_REMOTES, + buildAddressPaymentsQuery, + variables, + ); return formatPaymentData(data); } catch (error: any) { diff --git a/src/lib/queries/channel.ts b/src/lib/queries/channel.ts index 55fec30..59e78e4 100644 --- a/src/lib/queries/channel.ts +++ b/src/lib/queries/channel.ts @@ -4,32 +4,24 @@ import { gql } from "graphql-request"; import { graphQLClient } from "../graphQlClient"; import type { Channel, Transaction } from "../types"; -export const CHANNEL_QUERY = gql` +type ChannelSource = "storage" | "storage_sepolia"; + +const CHANNEL_SOURCES: readonly ChannelSource[] = [ + "storage", + "storage_sepolia", +]; + +/** + * Builds a single-remote channel query. + * + * Hasura returns `data: null` for the WHOLE document when any remote schema + * errors, so querying `storage` and `storage_sepolia` in one document meant an + * unhealthy sepolia remote took down server rendering of a healthy mainnet + * request. One document per remote isolates that failure. + */ +const buildChannelQuery = (source: ChannelSource) => gql` query ChannelQuery($id: ID!) @cached { - storage { - channel(id: $id) { - id - topics - transactions { - data - blockNumber - blockTimestamp - channelId - dataHash - encryptedData - encryptedKeys - encryptionMethod - hash - id - publicKeys - size - smartContractAddress - topics - transactionHash - } - } - } - storage_sepolia { + ${source} { channel(id: $id) { id topics @@ -55,53 +47,74 @@ export const CHANNEL_QUERY = gql` } `; +export const CHANNEL_QUERY = buildChannelQuery("storage"); +const CHANNEL_SEPOLIA_QUERY = buildChannelQuery("storage_sepolia"); + +const CHANNEL_QUERIES: Record = { + storage: CHANNEL_QUERY, + storage_sepolia: CHANNEL_SEPOLIA_QUERY, +}; + +const formatChannel = (channel: Channel, source: ChannelSource): Channel => ({ + ...channel, + source, + transactions: channel.transactions.map((transaction: Transaction) => { + try { + return { + ...transaction, + dataObject: JSON.parse(transaction.data), + }; + } catch (error: any) { + console.error(`Error parsing transaction data: ${error.message}`); + return transaction; + } + }), +}); + +/** + * Fetches a channel from both storage remotes independently. + * + * `null` means a hard "this request does not exist" (the caller turns it into + * `notFound()` + `noindex`), so it is only returned when EVERY remote answered + * successfully and none of them had the channel. If the channel was not found + * but a remote failed, "absent" and "unknown" are indistinguishable, so we + * throw and let the error boundary handle it rather than telling a crawler a + * possibly-live request does not exist. + */ export const fetchRequest = async (variables: { id: string; }): Promise => { - const data: { - storage: { channel: Channel }; - storage_sepolia: { channel: Channel }; - } = await graphQLClient.request(CHANNEL_QUERY, variables); - - // Check which storage has the channel - if (data?.storage?.channel) { - return { - ...data.storage.channel, - source: "storage", - transactions: data.storage.channel.transactions.map( - (transaction: Transaction) => { - try { - return { - ...transaction, - dataObject: JSON.parse(transaction.data), - }; - } catch (error: any) { - console.error(`Error parsing transaction data: ${error.message}`); - return transaction; - } - }, + const results = await Promise.allSettled( + CHANNEL_SOURCES.map((source) => + graphQLClient.request<{ [x: string]: { channel: Channel | null } }>( + CHANNEL_QUERIES[source], + variables, ), - }; + ), + ); + + const failedSources: string[] = []; + + for (let index = 0; index < CHANNEL_SOURCES.length; index++) { + const source = CHANNEL_SOURCES[index]; + const result = results[index]; + + if (result.status === "rejected") { + failedSources.push(source); + console.error(`fetchRequest: ${source} query failed`, result.reason); + continue; + } + + const channel = result.value?.[source]?.channel; + if (channel) { + return formatChannel(channel, source); + } } - if (data?.storage_sepolia?.channel) { - return { - ...data.storage_sepolia.channel, - source: "storage_sepolia", - transactions: data.storage_sepolia.channel.transactions.map( - (transaction: Transaction) => { - try { - return { - ...transaction, - dataObject: JSON.parse(transaction.data), - }; - } catch (error: any) { - console.error(`Error parsing transaction data: ${error.message}`); - return transaction; - } - }, - ), - }; + if (failedSources.length > 0) { + throw new Error( + `fetchRequest: channel ${variables.id} not found, but ${failedSources.length} of ${CHANNEL_SOURCES.length} storage queries failed (${failedSources.join(", ")}) — cannot tell "absent" from "unavailable"`, + ); } return null; diff --git a/src/lib/queries/payments.ts b/src/lib/queries/payments.ts index 1ac548b..1d63d6f 100644 --- a/src/lib/queries/payments.ts +++ b/src/lib/queries/payments.ts @@ -1,146 +1,15 @@ /** @format */ import { gql } from "graphql-request"; -import { graphQLClient } from "../graphQlClient"; +import { PAYMENT_CHAIN_REMOTES } from "../consts"; import type { Payment } from "../types"; import { formatPaymentData } from "../utils"; -import { CORE_PAYMENT_FIELDS } from "./utils"; +import { CORE_PAYMENT_FIELDS, requestPerChain } from "./utils"; -export const PAYMENTS_QUERY = gql` +export const buildPaymentsQuery = (chain: string) => gql` ${CORE_PAYMENT_FIELDS} query PaymentsQuery($first: Int, $skip: Int!) { - # - payment_mainnet { - payments( - first: $first - skip: $skip - orderBy: timestamp - orderDirection: desc - ) { - ...PaymentFields - } - } - payment_arbitrum_one { - payments( - first: $first - skip: $skip - orderBy: timestamp - orderDirection: desc - ) { - ...PaymentFields - } - } - payment_avalanche { - payments( - first: $first - skip: $skip - orderBy: timestamp - orderDirection: desc - ) { - ...PaymentFields - } - } - payment_base { - payments( - first: $first - skip: $skip - orderBy: timestamp - orderDirection: desc - ) { - ...PaymentFields - } - } - payment_bsc { - payments( - first: $first - skip: $skip - orderBy: timestamp - orderDirection: desc - ) { - ...PaymentFields - } - } - payment_celo { - payments( - first: $first - skip: $skip - orderBy: timestamp - orderDirection: desc - ) { - ...PaymentFields - } - } - payment_fantom { - payments( - first: $first - skip: $skip - orderBy: timestamp - orderDirection: desc - ) { - ...PaymentFields - } - } - payment_fuse { - payments( - first: $first - skip: $skip - orderBy: timestamp - orderDirection: desc - ) { - ...PaymentFields - } - } - payment_matic { - payments( - first: $first - skip: $skip - orderBy: timestamp - orderDirection: desc - ) { - ...PaymentFields - } - } - payment_moonbeam { - payments( - first: $first - skip: $skip - orderBy: timestamp - orderDirection: desc - ) { - ...PaymentFields - } - } - payment_optimism { - payments( - first: $first - skip: $skip - orderBy: timestamp - orderDirection: desc - ) { - ...PaymentFields - } - } - payment_sepolia { - payments( - first: $first - skip: $skip - orderBy: timestamp - orderDirection: desc - ) { - ...PaymentFields - } - } - payment_xdai { - payments( - first: $first - skip: $skip - orderBy: timestamp - orderDirection: desc - ) { - ...PaymentFields - } - } - payment_zksyncera { + ${chain} { payments( first: $first skip: $skip @@ -157,8 +26,12 @@ export const fetchPayments = async (variables: { first: number; skip: number; }): Promise => { - const data: { [x: string]: { payments: Payment[] } } = - await graphQLClient.request(PAYMENTS_QUERY, variables); + const data = await requestPerChain<{ payments: Payment[] }>( + "fetchPayments", + PAYMENT_CHAIN_REMOTES, + buildPaymentsQuery, + variables, + ); return formatPaymentData(data); }; diff --git a/src/lib/queries/request-payments.ts b/src/lib/queries/request-payments.ts index c488570..9c17d68 100644 --- a/src/lib/queries/request-payments.ts +++ b/src/lib/queries/request-payments.ts @@ -1,134 +1,16 @@ /** @format */ import { gql } from "graphql-request"; -import { graphQLClient } from "../graphQlClient"; +import { PAYMENT_CHAIN_REMOTES } from "../consts"; import type { Payment } from "../types"; import { formatPaymentData } from "../utils"; -import { CORE_PAYMENT_FIELDS } from "./utils"; +import { CORE_PAYMENT_FIELDS, requestPerChain } from "./utils"; -export const REQUEST_PAYMENTS_QUERY = gql` +export const buildRequestPaymentsQuery = (chain: string) => gql` ${CORE_PAYMENT_FIELDS} query RequestPaymentsQuery($reference: Bytes!) @cached { - # - payment_mainnet { - payments( - orderBy: timestamp - orderDirection: desc - where: { reference: $reference } - ) { - ...PaymentFields - } - } - payment_arbitrum_one { - payments( - where: { reference: $reference } - orderBy: timestamp - orderDirection: desc - ) { - ...PaymentFields - } - } - payment_avalanche { - payments( - where: { reference: $reference } - orderBy: timestamp - orderDirection: desc - ) { - ...PaymentFields - } - } - payment_base { - payments( - where: { reference: $reference } - orderBy: timestamp - orderDirection: desc - ) { - ...PaymentFields - } - } - payment_bsc { - payments( - where: { reference: $reference } - orderBy: timestamp - orderDirection: desc - ) { - ...PaymentFields - } - } - payment_celo { - payments( - where: { reference: $reference } - orderBy: timestamp - orderDirection: desc - ) { - ...PaymentFields - } - } - payment_fantom { - payments( - where: { reference: $reference } - orderBy: timestamp - orderDirection: desc - ) { - ...PaymentFields - } - } - payment_fuse { - payments( - where: { reference: $reference } - orderBy: timestamp - orderDirection: desc - ) { - ...PaymentFields - } - } - payment_matic { - payments( - where: { reference: $reference } - orderBy: timestamp - orderDirection: desc - ) { - ...PaymentFields - } - } - payment_moonbeam { - payments( - where: { reference: $reference } - orderBy: timestamp - orderDirection: desc - ) { - ...PaymentFields - } - } - payment_optimism { - payments( - where: { reference: $reference } - orderBy: timestamp - orderDirection: desc - ) { - ...PaymentFields - } - } - payment_sepolia { - payments( - where: { reference: $reference } - orderBy: timestamp - orderDirection: desc - ) { - ...PaymentFields - } - } - payment_xdai { - payments( - where: { reference: $reference } - orderBy: timestamp - orderDirection: desc - ) { - ...PaymentFields - } - } - payment_zksyncera { + ${chain} { payments( where: { reference: $reference } orderBy: timestamp @@ -144,10 +26,16 @@ export const fetchRequestPayments = async (variables: { reference: string; }): Promise => { try { - const data: { [x: string]: { payments: Payment[] } } | null = - variables.reference - ? await graphQLClient.request(REQUEST_PAYMENTS_QUERY, variables) - : null; + if (!variables.reference) { + return formatPaymentData(null); + } + + const data = await requestPerChain<{ payments: Payment[] }>( + "fetchRequestPayments", + PAYMENT_CHAIN_REMOTES, + buildRequestPaymentsQuery, + variables, + ); return formatPaymentData(data); } catch (error) { diff --git a/src/lib/queries/srf-deployments.ts b/src/lib/queries/srf-deployments.ts index 77570bc..ed6f0bc 100644 --- a/src/lib/queries/srf-deployments.ts +++ b/src/lib/queries/srf-deployments.ts @@ -1,113 +1,17 @@ import { gql } from "graphql-request"; -import { graphQLClient } from "../graphQlClient"; +import { SRF_CHAIN_REMOTES } from "../consts"; import type { SingleRequestProxyDeployment } from "../types"; import { formatProxyDeploymentData } from "../utils"; -import { CORE_PROXY_DEPLOYMENT_FIELDS } from "./utils"; +import { CORE_PROXY_DEPLOYMENT_FIELDS, requestPerChain } from "./utils"; -export const PROXY_DEPLOYMENTS_QUERY = gql` +type ProxyDeploymentsResult = { + singleRequestProxyDeployments: SingleRequestProxyDeployment[]; +}; + +export const buildProxyDeploymentsQuery = (chain: string) => gql` ${CORE_PROXY_DEPLOYMENT_FIELDS} query ProxyDeploymentsQuery($first: Int!, $skip: Int!) { - payment_mainnet { - singleRequestProxyDeployments( - first: $first - skip: $skip - orderBy: timestamp - orderDirection: desc - ) { - ...ProxyDeploymentFields - } - } - payment_arbitrum_one { - singleRequestProxyDeployments( - first: $first - skip: $skip - orderBy: timestamp - orderDirection: desc - ) { - ...ProxyDeploymentFields - } - } - payment_avalanche { - singleRequestProxyDeployments( - first: $first - skip: $skip - orderBy: timestamp - orderDirection: desc - ) { - ...ProxyDeploymentFields - } - } - payment_base { - singleRequestProxyDeployments( - first: $first - skip: $skip - orderBy: timestamp - orderDirection: desc - ) { - ...ProxyDeploymentFields - } - } - payment_bsc { - singleRequestProxyDeployments( - first: $first - skip: $skip - orderBy: timestamp - orderDirection: desc - ) { - ...ProxyDeploymentFields - } - } - payment_celo { - singleRequestProxyDeployments( - first: $first - skip: $skip - orderBy: timestamp - orderDirection: desc - ) { - ...ProxyDeploymentFields - } - } - payment_matic { - singleRequestProxyDeployments( - first: $first - skip: $skip - orderBy: timestamp - orderDirection: desc - ) { - ...ProxyDeploymentFields - } - } - payment_optimism { - singleRequestProxyDeployments( - first: $first - skip: $skip - orderBy: timestamp - orderDirection: desc - ) { - ...ProxyDeploymentFields - } - } - payment_sepolia { - singleRequestProxyDeployments( - first: $first - skip: $skip - orderBy: timestamp - orderDirection: desc - ) { - ...ProxyDeploymentFields - } - } - payment_xdai { - singleRequestProxyDeployments( - first: $first - skip: $skip - orderBy: timestamp - orderDirection: desc - ) { - ...ProxyDeploymentFields - } - } - payment_zksyncera { + ${chain} { singleRequestProxyDeployments( first: $first skip: $skip @@ -124,74 +28,20 @@ export const fetchProxyDeployments = async (variables: { first: number; skip: number; }): Promise => { - try { - const data: { - [x: string]: { - singleRequestProxyDeployments: SingleRequestProxyDeployment[]; - }; - } = await graphQLClient.request(PROXY_DEPLOYMENTS_QUERY, variables); + const data = await requestPerChain( + "fetchProxyDeployments", + SRF_CHAIN_REMOTES, + buildProxyDeploymentsQuery, + variables, + ); - return formatProxyDeploymentData(data); - } catch (error) { - console.error("Error fetching proxy deployments:", error); - return []; - } + return formatProxyDeploymentData(data); }; -export const PROXY_DEPLOYMENTS_BY_REFERENCE_QUERY = gql` +export const buildProxyDeploymentsByReferenceQuery = (chain: string) => gql` ${CORE_PROXY_DEPLOYMENT_FIELDS} query ProxyDeploymentsByReferenceQuery($reference: Bytes!) { - payment_mainnet { - singleRequestProxyDeployments(where: { paymentReference: $reference }) { - ...ProxyDeploymentFields - } - } - payment_arbitrum_one { - singleRequestProxyDeployments(where: { paymentReference: $reference }) { - ...ProxyDeploymentFields - } - } - payment_avalanche { - singleRequestProxyDeployments(where: { paymentReference: $reference }) { - ...ProxyDeploymentFields - } - } - payment_base { - singleRequestProxyDeployments(where: { paymentReference: $reference }) { - ...ProxyDeploymentFields - } - } - payment_bsc { - singleRequestProxyDeployments(where: { paymentReference: $reference }) { - ...ProxyDeploymentFields - } - } - payment_celo { - singleRequestProxyDeployments(where: { paymentReference: $reference }) { - ...ProxyDeploymentFields - } - } - payment_matic { - singleRequestProxyDeployments(where: { paymentReference: $reference }) { - ...ProxyDeploymentFields - } - } - payment_optimism { - singleRequestProxyDeployments(where: { paymentReference: $reference }) { - ...ProxyDeploymentFields - } - } - payment_sepolia { - singleRequestProxyDeployments(where: { paymentReference: $reference }) { - ...ProxyDeploymentFields - } - } - payment_xdai { - singleRequestProxyDeployments(where: { paymentReference: $reference }) { - ...ProxyDeploymentFields - } - } - payment_zksyncera { + ${chain} { singleRequestProxyDeployments(where: { paymentReference: $reference }) { ...ProxyDeploymentFields } @@ -202,19 +52,12 @@ export const PROXY_DEPLOYMENTS_BY_REFERENCE_QUERY = gql` export const fetchProxyDeploymentsByReference = async (variables: { reference: string; }): Promise => { - try { - const data: { - [x: string]: { - singleRequestProxyDeployments: SingleRequestProxyDeployment[]; - }; - } = await graphQLClient.request( - PROXY_DEPLOYMENTS_BY_REFERENCE_QUERY, - variables, - ); + const data = await requestPerChain( + "fetchProxyDeploymentsByReference", + SRF_CHAIN_REMOTES, + buildProxyDeploymentsByReferenceQuery, + variables, + ); - return formatProxyDeploymentData(data); - } catch (err) { - console.error("Error fetching proxy deployments by reference", err); - return []; - } + return formatProxyDeploymentData(data); }; diff --git a/src/lib/queries/utils.test.ts b/src/lib/queries/utils.test.ts new file mode 100644 index 0000000..cad4b83 --- /dev/null +++ b/src/lib/queries/utils.test.ts @@ -0,0 +1,97 @@ +/** @format */ + +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { requestPerChain } from "./utils"; + +const { request } = vi.hoisted(() => ({ request: vi.fn() })); + +vi.mock("../graphQlClient", () => ({ + graphQLClient: { + request, + }, +})); + +const buildQuery = (chain: string) => `query { ${chain} }`; + +describe("requestPerChain", () => { + beforeEach(() => { + request.mockReset(); + }); + + it("merges one entry per chain when all chains succeed", async () => { + request.mockImplementation((query: string) => { + if (query.includes("mainnet")) + return Promise.resolve({ mainnet: { total: 1 } }); + if (query.includes("polygon")) + return Promise.resolve({ polygon: { total: 2 } }); + return Promise.reject(new Error("unexpected chain")); + }); + + const result = await requestPerChain( + "test", + ["mainnet", "polygon"], + buildQuery, + {}, + ); + + expect(result).toEqual({ + mainnet: { total: 1 }, + polygon: { total: 2 }, + }); + }); + + it("skips a rejected chain, keeps the others, and does not throw", async () => { + request.mockImplementation((query: string) => { + if (query.includes("mainnet")) return Promise.reject(new Error("boom")); + if (query.includes("polygon")) + return Promise.resolve({ polygon: { total: 2 } }); + return Promise.reject(new Error("unexpected chain")); + }); + + const result = await requestPerChain( + "test", + ["mainnet", "polygon"], + buildQuery, + {}, + ); + + expect(result).toEqual({ polygon: { total: 2 } }); + }); + + it("skips a chain that resolves without its own bucket, and does not throw", async () => { + request.mockImplementation((query: string) => { + if (query.includes("mainnet")) return Promise.resolve({}); + if (query.includes("polygon")) + return Promise.resolve({ polygon: { total: 2 } }); + return Promise.reject(new Error("unexpected chain")); + }); + + const result = await requestPerChain( + "test", + ["mainnet", "polygon"], + buildQuery, + {}, + ); + + expect(result).toEqual({ polygon: { total: 2 } }); + }); + + it("throws naming the failed chains when every chain fails", async () => { + request.mockImplementation((query: string) => { + if (query.includes("mainnet")) return Promise.reject(new Error("boom")); + if (query.includes("polygon")) return Promise.reject(new Error("kaboom")); + return Promise.reject(new Error("unexpected chain")); + }); + + await expect( + requestPerChain("test", ["mainnet", "polygon"], buildQuery, {}), + ).rejects.toThrow(/mainnet.*polygon|polygon.*mainnet/); + }); + + it("does not throw for an empty chains array", async () => { + const result = await requestPerChain("test", [], buildQuery, {}); + + expect(result).toEqual({}); + expect(request).not.toHaveBeenCalled(); + }); +}); diff --git a/src/lib/queries/utils.ts b/src/lib/queries/utils.ts index 2f07b1d..be31531 100644 --- a/src/lib/queries/utils.ts +++ b/src/lib/queries/utils.ts @@ -1,6 +1,7 @@ /** @format */ import { gql } from "graphql-request"; +import { graphQLClient } from "../graphQlClient"; import type { SingleRequestProxyDeployment } from "../types"; export const CORE_PAYMENT_FIELDS = gql` @@ -26,6 +27,67 @@ export const CORE_PAYMENT_FIELDS = gql` } `; +/** + * Runs one GraphQL request per chain remote instead of a single document that + * aliases every chain at once. + * + * Hasura returns `data: null` for the WHOLE document when any remote schema + * errors or is unavailable, so a single unhealthy subgraph used to wipe out the + * data of every healthy chain. Splitting the fan-out isolates that failure: + * rejected chains are logged and skipped, and the merged result keeps whatever + * the healthy chains returned. + * + * The merged object has the same shape as the old combined response (one entry + * per `payment_*` alias), so the existing `formatPaymentData` / + * `formatProxyDeploymentData` merge + sort logic is reused untouched. + * + * If EVERY chain fails the error is rethrown: a total outage must stay + * distinguishable from "there is no data", so react-query reports `error` + * instead of rendering an empty table. + */ +export const requestPerChain = async ( + label: string, + chains: readonly string[], + buildQuery: (chain: string) => string, + variables: Record, +): Promise<{ [x: string]: T }> => { + const results = await Promise.allSettled( + chains.map((chain) => + graphQLClient.request<{ [x: string]: T }>(buildQuery(chain), variables), + ), + ); + + const merged: { [x: string]: T } = {}; + const failedChains: string[] = []; + + results.forEach((result, index) => { + const chain = chains[index]; + + if (result.status === "rejected") { + failedChains.push(chain); + console.error(`${label}: skipping chain ${chain}`, result.reason); + return; + } + + const chainData = result.value?.[chain]; + if (!chainData) { + failedChains.push(chain); + console.error(`${label}: skipping chain ${chain}, no data returned`); + return; + } + + merged[chain] = chainData; + }); + + if (chains.length > 0 && failedChains.length === chains.length) { + throw new Error( + `${label}: all ${chains.length} chain queries failed (${failedChains.join(", ")})`, + ); + } + + return merged; +}; + export const CORE_PROXY_DEPLOYMENT_FIELDS = gql` fragment ProxyDeploymentFields on SingleRequestProxyDeployment { id diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..e6480f4 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,15 @@ +/** @format */ + +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + resolve: { + alias: { + "@": new URL("./src", import.meta.url).pathname, + }, + }, + test: { + environment: "node", + include: ["src/**/*.test.ts"], + }, +});