From 327a24006f070b3ff2c038e7ef4ddd374afb8411 Mon Sep 17 00:00:00 2001 From: rodrigopavezi Date: Tue, 28 Jul 2026 11:52:58 -0300 Subject: [PATCH 1/3] fix: one dead subgraph no longer wipes out all payment data MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The payment and SRF queries were single GraphQL documents aliasing 11-14 chain remotes at once. Hasura returns `data: null` when any one remote errors, so a single unavailable chain destroyed the entire multi-chain result. That is not hypothetical — it is live on scan.request.network today: fetchRequestPayments Error: subgraph not found: no allocations field 'singleRequestProxyDeployments' not found in type: 'payment_zksynceraQuery' Consequences, confirmed by loading the site: /payments renders completely empty, "Recent payments" shows "No data", and every request page reports `Balance: 0` no matter what was actually paid. Thirteen healthy chains were being discarded because of one. Each fan-out is now one request per chain via Promise.allSettled, merged through the existing unchanged formatters. A rejected chain is skipped and logged with its name; healthy chains still return their data. Only a total failure of every chain throws — a full outage must not be indistinguishable from "no results", and the error states added in the previous commit surface it. Output is byte-identical when all chains are healthy. Worth stating plainly: today's behaviour is already per-chain top-N, not global top-N — `first: 10` across 14 chains can return 140 rows, because the formatters merge and sort but never apply a global slice. That is preserved rather than "fixed", since changing it would alter visible row counts and pagination. It deserves its own decision. Also here: - Drop payment_zksyncera from the SRF queries only. That subgraph genuinely lacks the singleRequestProxyDeployments entity, so querying it could only ever error. Expressed as data in consts.ts via the pre-existing but unused PAYMENT_CHAINS enum. Note SRF_CHAIN_REMOTES is not "all chains minus zksyncera" — the SRF queries never covered fantom, fuse or moonbeam, so an explicit list was the only way to preserve today's coverage exactly. - src/lib/hooks/payments.ts was a byte-for-byte duplicate of queries/payments.ts whose fetchPayments swallowed errors and returned [], making failure indistinguishable from empty at the query layer. Nothing imports it; it is now a thin re-export of the fixed implementation rather than a second copy of the same bug. - Add a CI smoke check that actually serves the app and requests /request/, failing on 5xx. This is the signal that was missing: `next build` only compiles that route (no generateStaticParams, so it is server-rendered on demand and never requested during the build), `npm run check` is formatting only, and the repo has no tests. The previous commit's bug would have been caught by this and by nothing else in CI. Trade-off accepted deliberately: up to 14 parallel HTTP round-trips per logical query instead of 1. `revalidate: 60` and Hasura's `@cached` blunt it, and no batching layer or dependency was added. Worth watching Hasura rate limits. Not addressed here: queries/transactions.ts and address-transactions.ts have the same all-or-nothing fragility against the two storage remotes (if storage_sepolia is down, mainnet requests vanish too). Left alone to keep this diff scoped; they need the same treatment. Restoring the missing subgraph itself is upstream indexer work, not a change in this repo. This commit stops one dead chain from zeroing the UI; it does not bring the data back. --- .github/workflows/build-and-lint.yml | 41 ++++++ src/lib/consts.ts | 29 ++++ src/lib/hooks/payments.ts | 177 ++--------------------- src/lib/queries/address-payments.ts | 159 ++------------------- src/lib/queries/payments.ts | 147 ++----------------- src/lib/queries/request-payments.ts | 140 ++---------------- src/lib/queries/srf-deployments.ts | 205 ++++----------------------- src/lib/queries/utils.ts | 62 ++++++++ 8 files changed, 200 insertions(+), 760 deletions(-) diff --git a/.github/workflows/build-and-lint.yml b/.github/workflows/build-and-lint.yml index 221a2c1..ca53db0 100644 --- a/.github/workflows/build-and-lint.yml +++ b/.github/workflows/build-and-lint.yml @@ -92,3 +92,44 @@ 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: 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/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/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.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 From 558411cb3f0c13c7d364e89ac6193777ed4b616d Mon Sep 17 00:00:00 2001 From: rodrigopavezi Date: Tue, 28 Jul 2026 12:57:53 -0300 Subject: [PATCH 2/3] fix: one failing storage remote no longer breaks the request page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Greptile P1 on the server-rendering PR: fetchRequest sent a single document covering both the storage and storage_sepolia remotes. Hasura nulls the whole document when any one remote errors, graphql-request then throws, and once that call moved into the server component a hiccup in the sepolia remote took down server rendering for a perfectly good mainnet request. The two remotes are now queried independently and settled with Promise.allSettled, so either one can fail without taking the other with it. The subtle part is what "not found" is allowed to mean. null from this function now drives notFound() plus robots: noindex, so it must mean "this request does not exist" and nothing else: - either remote returns the channel -> return it, even if the other failed - both answered, neither has it -> null, a genuine not-found - not found but a remote failed -> throw - both failed -> throw That third case is the one worth spelling out. "Absent" and "unavailable" are indistinguishable there, and returning null would tell a crawler that a request which may well be live does not exist, and noindex a real page. Throwing routes it to the error boundary instead, which keeps the invariant this stack already established: a backend failure is never reported as "not found". The thrown error names the remotes that failed so it stays diagnosable. The per-transaction JSON.parse and its try/catch fallback are unchanged, just factored into one helper instead of being duplicated per remote. Signature and returned shape are untouched, so no caller changes. Note this fragility was pre-existing and is not limited to this file — queries/transactions.ts and address-transactions.ts still send combined storage documents. Fixing them is follow-up work; this file is urgent because it is the one now on the server-rendering path. --- src/lib/queries/channel.ts | 145 ++++++++++++++++++++----------------- 1 file changed, 79 insertions(+), 66 deletions(-) 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; From 49c6ea470ae84e3b273c94db82ef538683d982de Mon Sep 17 00:00:00 2001 From: rodrigopavezi Date: Tue, 28 Jul 2026 22:50:55 -0300 Subject: [PATCH 3/3] test: cover the per-chain merge behaviour MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Greptile P2: the smoke check only inspects HTTP status, so it stays green if the per-chain query or merge logic regresses and starts producing empty or partial payment data. Correct — that check was built to catch an import throwing under Node, and it cannot see this. Adds vitest and five tests for requestPerChain, covering the behaviours whose regression would otherwise be invisible: - every chain succeeds -> one entry per chain in the merged result - one chain rejects -> skipped, the others survive, no throw - one resolves with no bucket -> treated as a failure, still no throw - every chain fails -> throws, naming the failed chains - empty chain list -> does not throw The second and fourth are the load-bearing ones. The whole point of the per-chain split is that one dead subgraph stops wiping out the other thirteen, and that a total outage stays distinguishable from "there is no data". Verified the tests can actually fail, twice and in different ways: making a rejected chain merge in as an empty bucket reds the skip test, and removing the total-failure throw reds the all-fail test. Both restored to byte-identical files afterwards, suite green again. A test that cannot fail would just recreate the blind spot being reported. vitest was needed rather than node:test because src/lib/graphQlClient.ts calls React's cache() at module scope and the installed react@18.3.1 does not export it — that only works under Next, which aliases react to its own bundled fork — so any runner that lets the real module execute dies on load. vi.mock keeps it from ever being evaluated. graphql-request v7 is also ESM-only, which vitest handles natively. Wired into CI ahead of the smoke test, so a logic regression fails fast and with a clearer signal than a server probe. Worth being explicit about what this does not do: it tests the merge logic in isolation, not live behaviour against real subgraphs. Catching that needs a mock backend in CI, which is a larger piece of work. --- .github/workflows/build-and-lint.yml | 3 + package-lock.json | 1297 +++++++++++++++++++++++++- package.json | 6 +- src/lib/queries/utils.test.ts | 97 ++ vitest.config.ts | 15 + 5 files changed, 1374 insertions(+), 44 deletions(-) create mode 100644 src/lib/queries/utils.test.ts create mode 100644 vitest.config.ts diff --git a/.github/workflows/build-and-lint.yml b/.github/workflows/build-and-lint.yml index ca53db0..8c15e81 100644 --- a/.github/workflows/build-and-lint.yml +++ b/.github/workflows/build-and-lint.yml @@ -97,6 +97,9 @@ jobs: 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" 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/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/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"], + }, +});