diff --git a/web/package.json b/web/package.json index efe863eb5..2224cf711 100644 --- a/web/package.json +++ b/web/package.json @@ -78,6 +78,8 @@ }, "dependencies": { "@cyntler/react-doc-viewer": "^1.17.0", + "@graphql-tools/batch-execute": "^9.0.11", + "@graphql-tools/utils": "^10.7.2", "@kleros/kleros-app": "workspace:^", "@kleros/kleros-sdk": "workspace:^", "@kleros/kleros-v2-contracts": "workspace:^", diff --git a/web/src/consts/index.ts b/web/src/consts/index.ts index 9592cedac..219ff0e9c 100644 --- a/web/src/consts/index.ts +++ b/web/src/consts/index.ts @@ -7,6 +7,7 @@ export { ArbitratorTypes }; export const ONE_BASIS_POINT = 10000n; export const REFETCH_INTERVAL = 5000; +export const STALE_TIME = 1000; export const IPFS_GATEWAY = import.meta.env.REACT_APP_IPFS_GATEWAY || "https://cdn.kleros.link"; export const HERMES_TELEGRAM_BOT_URL = @@ -20,7 +21,7 @@ export const GIT_URL = `https://github.com/kleros/kleros-v2/tree/${gitCommitHash export const RELEASE_VERSION = version; // https://www.w3.org/TR/2012/WD-html-markup-20120329/input.email.html#input.email.attrs.value.single -// eslint-disable-next-line security/detect-unsafe-regex + export const EMAIL_REGEX = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; export const TELEGRAM_REGEX = /^@\w{5,32}$/; diff --git a/web/src/context/GraphqlBatcher.tsx b/web/src/context/GraphqlBatcher.tsx index bd7b1572a..23ffdb1b4 100644 --- a/web/src/context/GraphqlBatcher.tsx +++ b/web/src/context/GraphqlBatcher.tsx @@ -1,12 +1,13 @@ import React, { useMemo, createContext, useContext } from "react"; +import { createBatchingExecutor } from "@graphql-tools/batch-execute"; +import { AsyncExecutor, ExecutionResult } from "@graphql-tools/utils"; import { TypedDocumentNode } from "@graphql-typed-document-node/core"; import { create, windowedFiniteBatchScheduler, Batcher } from "@yornaath/batshit"; import { request } from "graphql-request"; import { debounceErrorToast } from "utils/debounceErrorToast"; import { getGraphqlUrl } from "utils/getGraphqlUrl"; - interface IGraphqlBatcher { graphqlBatcher: Batcher; } @@ -21,19 +22,35 @@ interface IQuery { const Context = createContext(undefined); +const executor: AsyncExecutor = async ({ document, variables, extensions }) => { + try { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore + const result = request(extensions.url, document, variables).then((res) => ({ + data: res, + })) as Promise; + + return result; + } catch (error) { + console.error("Graph error: ", { error }); + debounceErrorToast("Graph query error: failed to fetch data."); + return { data: {} }; + } +}; + +const batchExec = createBatchingExecutor(executor); + const fetcher = async (queries: IQuery[]) => { - const promises = queries.map(async ({ id, document, variables, isDisputeTemplate, chainId }) => { - const url = getGraphqlUrl(isDisputeTemplate ?? false, chainId); - try { - return request(url, document, variables).then((result) => ({ id, result })); - } catch (error) { - console.error("Graph error: ", { error }); - debounceErrorToast("Graph query error: failed to fetch data."); - return { id, result: {} }; - } - }); - const data = await Promise.all(promises); - return data; + const batchdata = await Promise.all( + queries.map(({ document, variables, isDisputeTemplate, chainId }) => + batchExec({ document, variables, extensions: { url: getGraphqlUrl(isDisputeTemplate ?? false, chainId) } }) + ) + ); + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore + const processedData = batchdata.map((data, index) => ({ id: queries[index].id, result: data.data })); + return processedData; }; const GraphqlBatcherProvider: React.FC<{ children?: React.ReactNode }> = ({ children }) => { diff --git a/web/src/hooks/queries/useAllCasesQuery.ts b/web/src/hooks/queries/useAllCasesQuery.ts index 49cc787c8..e24bb2102 100644 --- a/web/src/hooks/queries/useAllCasesQuery.ts +++ b/web/src/hooks/queries/useAllCasesQuery.ts @@ -2,6 +2,7 @@ import { useQuery } from "@tanstack/react-query"; import { useGraphqlBatcher } from "context/GraphqlBatcher"; +import { STALE_TIME } from "src/consts"; import { graphql } from "src/graphql"; import { AllCasesQuery } from "src/graphql/graphql"; @@ -20,6 +21,7 @@ export const useAllCasesQuery = () => { const { graphqlBatcher } = useGraphqlBatcher(); return useQuery({ queryKey: [`allCasesQuery`], + staleTime: STALE_TIME, queryFn: async () => await graphqlBatcher.fetch({ id: crypto.randomUUID(), document: allCasesQuery, variables: {} }), }); diff --git a/web/src/hooks/queries/useClassicAppealQuery.ts b/web/src/hooks/queries/useClassicAppealQuery.ts index d12a76238..8a5ddf073 100644 --- a/web/src/hooks/queries/useClassicAppealQuery.ts +++ b/web/src/hooks/queries/useClassicAppealQuery.ts @@ -1,6 +1,6 @@ import { useQuery } from "@tanstack/react-query"; -import { REFETCH_INTERVAL } from "consts/index"; +import { REFETCH_INTERVAL, STALE_TIME } from "consts/index"; import { useGraphqlBatcher } from "context/GraphqlBatcher"; import { graphql } from "src/graphql"; @@ -44,6 +44,7 @@ export const useClassicAppealQuery = (id?: string | number) => { queryKey: [`classicAppealQuery${id}`], enabled: isEnabled, refetchInterval: REFETCH_INTERVAL, + staleTime: STALE_TIME, queryFn: async () => isEnabled ? await graphqlBatcher.fetch({ diff --git a/web/src/hooks/queries/useCourtDetails.ts b/web/src/hooks/queries/useCourtDetails.ts index 296d4aa09..321c6ce33 100644 --- a/web/src/hooks/queries/useCourtDetails.ts +++ b/web/src/hooks/queries/useCourtDetails.ts @@ -1,6 +1,6 @@ import { useQuery } from "@tanstack/react-query"; -import { REFETCH_INTERVAL } from "consts/index"; +import { REFETCH_INTERVAL, STALE_TIME } from "consts/index"; import { useGraphqlBatcher } from "context/GraphqlBatcher"; import { graphql } from "src/graphql"; @@ -36,6 +36,7 @@ export const useCourtDetails = (id?: string) => { queryKey: [`courtDetails${id}`], enabled: isEnabled, refetchInterval: REFETCH_INTERVAL, + staleTime: STALE_TIME, queryFn: async () => await graphqlBatcher.fetch({ id: crypto.randomUUID(), document: courtDetailsQuery, variables: { id } }), }); diff --git a/web/src/hooks/queries/useCourtTree.ts b/web/src/hooks/queries/useCourtTree.ts index dffab3e2a..5eb70814d 100644 --- a/web/src/hooks/queries/useCourtTree.ts +++ b/web/src/hooks/queries/useCourtTree.ts @@ -2,6 +2,7 @@ import { useQuery } from "@tanstack/react-query"; import { useGraphqlBatcher } from "context/GraphqlBatcher"; +import { STALE_TIME } from "src/consts"; import { graphql } from "src/graphql"; import { CourtTreeQuery } from "src/graphql/graphql"; export type { CourtTreeQuery }; @@ -39,6 +40,7 @@ export const useCourtTree = () => { const { graphqlBatcher } = useGraphqlBatcher(); return useQuery({ queryKey: ["courtTreeQuery"], + staleTime: STALE_TIME, queryFn: async () => await graphqlBatcher.fetch({ id: crypto.randomUUID(), document: courtTreeQuery, variables: {} }), }); diff --git a/web/src/hooks/queries/useDisputeDetailsQuery.ts b/web/src/hooks/queries/useDisputeDetailsQuery.ts index 71a417904..2aed81c0f 100644 --- a/web/src/hooks/queries/useDisputeDetailsQuery.ts +++ b/web/src/hooks/queries/useDisputeDetailsQuery.ts @@ -1,6 +1,6 @@ import { useQuery } from "@tanstack/react-query"; -import { REFETCH_INTERVAL } from "consts/index"; +import { REFETCH_INTERVAL, STALE_TIME } from "consts/index"; import { useGraphqlBatcher } from "context/GraphqlBatcher"; import { graphql } from "src/graphql"; @@ -48,6 +48,7 @@ export const useDisputeDetailsQuery = (id?: string | number) => { queryKey: [`disputeDetailsQuery${id}`], enabled: isEnabled, refetchInterval: REFETCH_INTERVAL, + staleTime: STALE_TIME, queryFn: async () => await graphqlBatcher.fetch({ id: crypto.randomUUID(), diff --git a/web/src/hooks/queries/useDisputeMaintenanceQuery.ts b/web/src/hooks/queries/useDisputeMaintenanceQuery.ts index 0703804d2..191684c2f 100644 --- a/web/src/hooks/queries/useDisputeMaintenanceQuery.ts +++ b/web/src/hooks/queries/useDisputeMaintenanceQuery.ts @@ -2,6 +2,7 @@ import { useQuery } from "@tanstack/react-query"; import { useGraphqlBatcher } from "context/GraphqlBatcher"; +import { STALE_TIME } from "src/consts"; import { graphql } from "src/graphql"; import { DisputeMaintenanceQuery } from "src/graphql/graphql"; import { isUndefined } from "src/utils"; @@ -40,6 +41,7 @@ const useDisputeMaintenanceQuery = (id?: string) => { return useQuery({ queryKey: [`disputeMaintenanceQuery-${id}`], enabled: isEnabled, + staleTime: STALE_TIME, queryFn: async () => await graphqlBatcher.fetch({ id: crypto.randomUUID(), diff --git a/web/src/hooks/queries/useJurorStakeDetailsQuery.ts b/web/src/hooks/queries/useJurorStakeDetailsQuery.ts index 51b54c51a..21fdc3d69 100644 --- a/web/src/hooks/queries/useJurorStakeDetailsQuery.ts +++ b/web/src/hooks/queries/useJurorStakeDetailsQuery.ts @@ -1,6 +1,6 @@ import { useQuery } from "@tanstack/react-query"; -import { REFETCH_INTERVAL } from "consts/index"; +import { REFETCH_INTERVAL, STALE_TIME } from "consts/index"; import { useGraphqlBatcher } from "context/GraphqlBatcher"; import { graphql } from "src/graphql"; @@ -29,6 +29,7 @@ export const useJurorStakeDetailsQuery = (userId?: string) => { queryKey: [`jurorStakeDetails${userId}`], enabled: isEnabled, refetchInterval: REFETCH_INTERVAL, + staleTime: STALE_TIME, queryFn: async () => await graphqlBatcher.fetch({ id: crypto.randomUUID(), document: jurorStakeDetailsQuery, variables: { userId } }), }); diff --git a/web/src/hooks/queries/useUser.ts b/web/src/hooks/queries/useUser.ts index d453035c9..5da4bc1e0 100644 --- a/web/src/hooks/queries/useUser.ts +++ b/web/src/hooks/queries/useUser.ts @@ -3,6 +3,7 @@ import { Address } from "viem"; import { useGraphqlBatcher } from "context/GraphqlBatcher"; +import { STALE_TIME } from "src/consts"; import { graphql } from "src/graphql"; import { UserQuery, Dispute_Filter, UserDisputeFilterQuery, UserDetailsFragment } from "src/graphql/graphql"; export type { UserQuery, UserDetailsFragment }; @@ -58,6 +59,7 @@ export const useUserQuery = (address?: Address, where?: Dispute_Filter) => { return useQuery({ queryKey: [`userQuery${address?.toLowerCase()}`], enabled: isEnabled, + staleTime: STALE_TIME, queryFn: async () => await graphqlBatcher.fetch({ id: crypto.randomUUID(), diff --git a/web/src/hooks/queries/useVotingHistory.ts b/web/src/hooks/queries/useVotingHistory.ts index e1210ddaa..cdebd9186 100644 --- a/web/src/hooks/queries/useVotingHistory.ts +++ b/web/src/hooks/queries/useVotingHistory.ts @@ -1,6 +1,6 @@ import { useQuery } from "@tanstack/react-query"; -import { REFETCH_INTERVAL } from "consts/index"; +import { REFETCH_INTERVAL, STALE_TIME } from "consts/index"; import { useGraphqlBatcher } from "context/GraphqlBatcher"; import { graphql } from "src/graphql"; @@ -59,6 +59,7 @@ export const useVotingHistory = (disputeID?: string) => { queryKey: [`VotingHistory${disputeID}`], enabled: isEnabled, refetchInterval: REFETCH_INTERVAL, + staleTime: STALE_TIME, queryFn: async () => await graphqlBatcher.fetch({ id: crypto.randomUUID(), document: votingHistoryQuery, variables: { disputeID } }), }); diff --git a/yarn.lock b/yarn.lock index 363126cb4..0b6abee22 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4510,6 +4510,19 @@ __metadata: languageName: node linkType: hard +"@graphql-tools/batch-execute@npm:^9.0.11": + version: 9.0.11 + resolution: "@graphql-tools/batch-execute@npm:9.0.11" + dependencies: + "@graphql-tools/utils": "npm:^10.7.0" + dataloader: "npm:^2.2.3" + tslib: "npm:^2.8.1" + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + checksum: 10/6180424a5fa36a446baa665a92cff0332a566b1bd7481e2641c9d0aa2a7a47a24d21a9b90bb3d7f4c0d5a7331fc9e623fe43746f07e5eb0654419a29d860a940 + languageName: node + linkType: hard + "@graphql-tools/code-file-loader@npm:^8.0.0": version: 8.0.1 resolution: "@graphql-tools/code-file-loader@npm:8.0.1" @@ -4837,6 +4850,20 @@ __metadata: languageName: node linkType: hard +"@graphql-tools/utils@npm:^10.7.0, @graphql-tools/utils@npm:^10.7.2": + version: 10.7.2 + resolution: "@graphql-tools/utils@npm:10.7.2" + dependencies: + "@graphql-typed-document-node/core": "npm:^3.1.1" + cross-inspect: "npm:1.0.1" + dset: "npm:^3.1.4" + tslib: "npm:^2.4.0" + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + checksum: 10/b4725b081e5ff5c1441036db76ce907a6fe9b4c94aa9ceb070f75541b2297c3cccaa182f91d214f9abe6d89df33d8df51e055afbc4e382b01e8d8fb7c2f6edf6 + languageName: node + linkType: hard + "@graphql-tools/wrap@npm:^10.0.0": version: 10.0.0 resolution: "@graphql-tools/wrap@npm:10.0.0" @@ -5609,6 +5636,8 @@ __metadata: "@eslint/js": "npm:^9.15.0" "@graphql-codegen/cli": "npm:^5.0.3" "@graphql-codegen/client-preset": "npm:^4.5.1" + "@graphql-tools/batch-execute": "npm:^9.0.11" + "@graphql-tools/utils": "npm:^10.7.2" "@kleros/kleros-app": "workspace:^" "@kleros/kleros-sdk": "workspace:^" "@kleros/kleros-v2-contracts": "workspace:^" @@ -15796,6 +15825,15 @@ __metadata: languageName: node linkType: hard +"cross-inspect@npm:1.0.1": + version: 1.0.1 + resolution: "cross-inspect@npm:1.0.1" + dependencies: + tslib: "npm:^2.4.0" + checksum: 10/7c1e02e0a9670b62416a3ea1df7ae880fdad3aa0a857de8932c4e5f8acd71298c7e3db9da8e9da603f5692cd1879938f5e72e34a9f5d1345987bef656d117fc1 + languageName: node + linkType: hard + "cross-spawn@npm:7.0.3": version: 7.0.3 resolution: "cross-spawn@npm:7.0.3" @@ -16314,6 +16352,13 @@ __metadata: languageName: node linkType: hard +"dataloader@npm:^2.2.3": + version: 2.2.3 + resolution: "dataloader@npm:2.2.3" + checksum: 10/83fe6259abe00ae64c5f48252ef59d8e5fcabda9fd4d26685f14a76eeca596bf6f9500d9f22a0094c50c3ea782a0977728f9367e232dfa0fdb5c9d646de279b2 + languageName: node + linkType: hard + "date-fns@npm:^1.27.2": version: 1.30.1 resolution: "date-fns@npm:1.30.1" @@ -17170,6 +17215,13 @@ __metadata: languageName: node linkType: hard +"dset@npm:^3.1.4": + version: 3.1.4 + resolution: "dset@npm:3.1.4" + checksum: 10/6268c9e2049c8effe6e5a1952f02826e8e32468b5ced781f15f8f3b1c290da37626246fec014fbdd1503413f981dff6abd8a4c718ec9952fd45fccb6ac9de43f + languageName: node + linkType: hard + "duplexer3@npm:^0.1.4": version: 0.1.5 resolution: "duplexer3@npm:0.1.5" @@ -34460,7 +34512,7 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^2.0.0, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.3.1, tslib@npm:^2.4.0, tslib@npm:^2.5.0, tslib@npm:^2.6.0, tslib@npm:^2.6.2, tslib@npm:^2.6.3": +"tslib@npm:^2.0.0, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.3.1, tslib@npm:^2.4.0, tslib@npm:^2.5.0, tslib@npm:^2.6.0, tslib@npm:^2.6.2, tslib@npm:^2.6.3, tslib@npm:^2.8.1": version: 2.8.1 resolution: "tslib@npm:2.8.1" checksum: 10/3e2e043d5c2316461cb54e5c7fe02c30ef6dccb3384717ca22ae5c6b5bc95232a6241df19c622d9c73b809bea33b187f6dbc73030963e29950c2141bc32a79f7