From 33c8afdf8ec4237c1187dcdb252e2fa072738fd5 Mon Sep 17 00:00:00 2001 From: ogzhanolguncu Date: Thu, 17 Apr 2025 15:21:20 +0300 Subject: [PATCH 1/5] refactor: RQ hook --- .../controls/components/logs-refresh.tsx | 2 ++ .../components/table/hooks/use-logs-query.ts | 11 ++++---- .../logs/components/table/hooks/utils.ts | 27 +++++++++++++++++++ 3 files changed, 35 insertions(+), 5 deletions(-) create mode 100644 apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/table/hooks/utils.ts diff --git a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/controls/components/logs-refresh.tsx b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/controls/components/logs-refresh.tsx index ba114836ad..db827662b6 100644 --- a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/controls/components/logs-refresh.tsx +++ b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/controls/components/logs-refresh.tsx @@ -2,6 +2,7 @@ import { RefreshButton } from "@/components/logs/refresh-button"; import { trpc } from "@/lib/trpc/client"; import { useRatelimitLogsContext } from "../../../context/logs"; import { useFilters } from "../../../hooks/use-filters"; +import { refreshQueryTimestamp } from "../../table/hooks/utils"; export const LogsRefresh = () => { const { toggleLive, isLive } = useRatelimitLogsContext(); @@ -10,6 +11,7 @@ export const LogsRefresh = () => { const hasRelativeFilter = filters.find((f) => f.field === "since"); const handleRefresh = () => { + refreshQueryTimestamp(); ratelimit.logs.query.invalidate(); ratelimit.logs.queryRatelimitTimeseries.invalidate(); }; diff --git a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/table/hooks/use-logs-query.ts b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/table/hooks/use-logs-query.ts index 73ead945b5..3bafadcb2a 100644 --- a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/table/hooks/use-logs-query.ts +++ b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/table/hooks/use-logs-query.ts @@ -4,6 +4,7 @@ import type { RatelimitLog } from "@unkey/clickhouse/src/ratelimits"; import { useCallback, useEffect, useMemo, useState } from "react"; import { useFilters } from "../../../hooks/use-filters"; import type { RatelimitQueryLogsPayload } from "../query-logs.schema"; +import { useQueryTimestamp } from "./utils"; type UseLogsQueryParams = { limit?: number; @@ -23,6 +24,8 @@ export function useRatelimitLogsQuery({ const [realtimeLogsMap, setRealtimeLogsMap] = useState(() => new Map()); const [totalCount, setTotalCount] = useState(0); + const timestamp = useQueryTimestamp(); + const { filters } = useFilters(); const queryClient = trpc.useUtils(); @@ -32,13 +35,11 @@ export function useRatelimitLogsQuery({ const historicalLogs = useMemo(() => Array.from(historicalLogsMap.values()), [historicalLogsMap]); - //Required for preventing double trpc call during initial render - const dateNow = useMemo(() => Date.now(), []); const queryParams = useMemo(() => { const params: RatelimitQueryLogsPayload = { limit, - startTime: dateNow - HISTORICAL_DATA_WINDOW, - endTime: dateNow, + startTime: timestamp - HISTORICAL_DATA_WINDOW, + endTime: timestamp, requestIds: { filters: [] }, identifiers: { filters: [] }, status: { filters: [] }, @@ -105,7 +106,7 @@ export function useRatelimitLogsQuery({ }); return params; - }, [filters, limit, dateNow, namespaceId]); + }, [filters, limit, namespaceId, timestamp]); // Main query for historical data const { diff --git a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/table/hooks/utils.ts b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/table/hooks/utils.ts new file mode 100644 index 0000000000..61ede24d07 --- /dev/null +++ b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/table/hooks/utils.ts @@ -0,0 +1,27 @@ +import { useEffect, useState } from "react"; + +let QUERY_TIMESTAMP = Date.now(); +let subscribers: ((timestamp: number) => void)[] = []; + +export const getQueryTimestamp = () => QUERY_TIMESTAMP; + +export const refreshQueryTimestamp = () => { + QUERY_TIMESTAMP = Date.now(); + subscribers.forEach((callback) => callback(QUERY_TIMESTAMP)); + return QUERY_TIMESTAMP; +}; + +export const useQueryTimestamp = () => { + const [timestamp, setTimestamp] = useState(QUERY_TIMESTAMP); + + useEffect(() => { + const callback = (newTimestamp: number) => setTimestamp(newTimestamp); + subscribers.push(callback); + + return () => { + subscribers = subscribers.filter((cb) => cb !== callback); + }; + }, []); + + return timestamp; +}; From 0b6b931b92d8b5fa32a9b7c89e1f3fba6536aeea Mon Sep 17 00:00:00 2001 From: ogzhanolguncu Date: Tue, 22 Apr 2025 17:19:09 +0300 Subject: [PATCH 2/5] feat: memoize timestamp for ratelimit queries --- apps/dashboard/app/(app)/layout.tsx | 3 +- .../bar-chart/hooks/use-fetch-timeseries.ts | 9 ++-- .../components/table/hooks/use-logs-query.ts | 10 ++-- .../charts/hooks/use-fetch-timeseries.ts | 9 ++-- .../controls/components/logs-refresh.tsx | 5 +- .../components/table/hooks/use-logs-query.ts | 4 +- .../providers/query-time-provider.tsx | 46 +++++++++++++++++++ 7 files changed, 69 insertions(+), 17 deletions(-) create mode 100644 apps/dashboard/providers/query-time-provider.tsx diff --git a/apps/dashboard/app/(app)/layout.tsx b/apps/dashboard/app/(app)/layout.tsx index 91d36a224d..c90af8707f 100644 --- a/apps/dashboard/app/(app)/layout.tsx +++ b/apps/dashboard/app/(app)/layout.tsx @@ -6,6 +6,7 @@ import { db } from "@/lib/db"; import { Empty } from "@unkey/ui"; import Link from "next/link"; import { redirect } from "next/navigation"; +import { QueryTimeProvider } from "../../providers/query-time-provider"; interface LayoutProps { children: React.ReactNode; @@ -49,7 +50,7 @@ export default async function Layout({ children }: LayoutProps) {
{workspace.enabled ? ( - children + {children} ) : (
diff --git a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/_overview/components/charts/bar-chart/hooks/use-fetch-timeseries.ts b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/_overview/components/charts/bar-chart/hooks/use-fetch-timeseries.ts index d834c0054d..381e63a0da 100644 --- a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/_overview/components/charts/bar-chart/hooks/use-fetch-timeseries.ts +++ b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/_overview/components/charts/bar-chart/hooks/use-fetch-timeseries.ts @@ -1,19 +1,20 @@ import { formatTimestampForChart } from "@/components/logs/chart/utils/format-timestamp"; import { TIMESERIES_DATA_WINDOW } from "@/components/logs/constants"; import { trpc } from "@/lib/trpc/client"; +import { useQueryTime } from "@/providers/query-time-provider"; import { useMemo } from "react"; import { useFilters } from "../../../../hooks/use-filters"; import type { RatelimitOverviewQueryTimeseriesPayload } from "../query-timeseries.schema"; export const useFetchRatelimitOverviewTimeseries = (namespaceId: string) => { const { filters } = useFilters(); - const dateNow = useMemo(() => Date.now(), []); + const { queryTime: timestamp } = useQueryTime(); const queryParams = useMemo(() => { const params: RatelimitOverviewQueryTimeseriesPayload = { namespaceId, - startTime: dateNow - TIMESERIES_DATA_WINDOW, - endTime: dateNow, + startTime: timestamp - TIMESERIES_DATA_WINDOW, + endTime: timestamp, identifiers: { filters: [] }, since: "", }; @@ -52,7 +53,7 @@ export const useFetchRatelimitOverviewTimeseries = (namespaceId: string) => { }); return params; - }, [filters, dateNow, namespaceId]); + }, [filters, timestamp, namespaceId]); const { data, isLoading, isError } = trpc.ratelimit.logs.queryRatelimitTimeseries.useQuery( queryParams, diff --git a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/_overview/components/table/hooks/use-logs-query.ts b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/_overview/components/table/hooks/use-logs-query.ts index 256ee3ec1c..bd9db15a39 100644 --- a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/_overview/components/table/hooks/use-logs-query.ts +++ b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/_overview/components/table/hooks/use-logs-query.ts @@ -1,5 +1,6 @@ import { HISTORICAL_DATA_WINDOW } from "@/components/logs/constants"; import { trpc } from "@/lib/trpc/client"; +import { useQueryTime } from "@/providers/query-time-provider"; import type { RatelimitOverviewLog } from "@unkey/clickhouse/src/ratelimits"; import { useEffect, useMemo, useState } from "react"; import { useSort } from "../../../../../../../../components/logs/hooks/use-sort"; @@ -18,18 +19,19 @@ export function useRatelimitOverviewLogsQuery({ namespaceId, limit = 50 }: UseLo ); const { filters } = useFilters(); + const { queryTime: timestamp } = useQueryTime(); const historicalLogs = useMemo(() => Array.from(historicalLogsMap.values()), [historicalLogsMap]); const { sorts } = useSort(); //Required for preventing double trpc call during initial render - const dateNow = useMemo(() => Date.now(), []); + const queryParams = useMemo(() => { const params: RatelimitQueryOverviewLogsPayload = { limit, - startTime: dateNow - HISTORICAL_DATA_WINDOW, - endTime: dateNow, + startTime: timestamp - HISTORICAL_DATA_WINDOW, + endTime: timestamp, identifiers: { filters: [] }, status: { filters: [] }, namespaceId, @@ -84,7 +86,7 @@ export function useRatelimitOverviewLogsQuery({ namespaceId, limit = 50 }: UseLo }); return params; - }, [filters, limit, dateNow, namespaceId, sorts]); + }, [filters, limit, timestamp, namespaceId, sorts]); // Main query for historical data const { diff --git a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/charts/hooks/use-fetch-timeseries.ts b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/charts/hooks/use-fetch-timeseries.ts index 33602ca29a..a5205f2324 100644 --- a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/charts/hooks/use-fetch-timeseries.ts +++ b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/charts/hooks/use-fetch-timeseries.ts @@ -1,19 +1,20 @@ import { formatTimestampForChart } from "@/components/logs/chart/utils/format-timestamp"; import { TIMESERIES_DATA_WINDOW } from "@/components/logs/constants"; import { trpc } from "@/lib/trpc/client"; +import { useQueryTime } from "@/providers/query-time-provider"; import { useMemo } from "react"; import { useFilters } from "../../../hooks/use-filters"; import type { RatelimitQueryTimeseriesPayload } from "../query-timeseries.schema"; export const useFetchRatelimitTimeseries = (namespaceId: string) => { const { filters } = useFilters(); - const dateNow = useMemo(() => Date.now(), []); + const { queryTime: timestamp } = useQueryTime(); const queryParams = useMemo(() => { const params: RatelimitQueryTimeseriesPayload = { namespaceId, - startTime: dateNow - TIMESERIES_DATA_WINDOW, - endTime: dateNow, + startTime: timestamp - TIMESERIES_DATA_WINDOW, + endTime: timestamp, identifiers: { filters: [] }, since: "", }; @@ -52,7 +53,7 @@ export const useFetchRatelimitTimeseries = (namespaceId: string) => { }); return params; - }, [filters, dateNow, namespaceId]); + }, [filters, namespaceId, timestamp]); const { data, isLoading, isError } = trpc.ratelimit.logs.queryRatelimitTimeseries.useQuery( queryParams, diff --git a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/controls/components/logs-refresh.tsx b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/controls/components/logs-refresh.tsx index db827662b6..3bf0b5cf5b 100644 --- a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/controls/components/logs-refresh.tsx +++ b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/controls/components/logs-refresh.tsx @@ -1,17 +1,18 @@ import { RefreshButton } from "@/components/logs/refresh-button"; import { trpc } from "@/lib/trpc/client"; +import { useQueryTime } from "@/providers/query-time-provider"; import { useRatelimitLogsContext } from "../../../context/logs"; import { useFilters } from "../../../hooks/use-filters"; -import { refreshQueryTimestamp } from "../../table/hooks/utils"; export const LogsRefresh = () => { const { toggleLive, isLive } = useRatelimitLogsContext(); + const { refreshQueryTime } = useQueryTime(); const { filters } = useFilters(); const { ratelimit } = trpc.useUtils(); const hasRelativeFilter = filters.find((f) => f.field === "since"); const handleRefresh = () => { - refreshQueryTimestamp(); + refreshQueryTime(); ratelimit.logs.query.invalidate(); ratelimit.logs.queryRatelimitTimeseries.invalidate(); }; diff --git a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/table/hooks/use-logs-query.ts b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/table/hooks/use-logs-query.ts index 3bafadcb2a..b2f9c6ba09 100644 --- a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/table/hooks/use-logs-query.ts +++ b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/table/hooks/use-logs-query.ts @@ -1,10 +1,10 @@ import { HISTORICAL_DATA_WINDOW } from "@/components/logs/constants"; import { trpc } from "@/lib/trpc/client"; +import { useQueryTime } from "@/providers/query-time-provider"; import type { RatelimitLog } from "@unkey/clickhouse/src/ratelimits"; import { useCallback, useEffect, useMemo, useState } from "react"; import { useFilters } from "../../../hooks/use-filters"; import type { RatelimitQueryLogsPayload } from "../query-logs.schema"; -import { useQueryTimestamp } from "./utils"; type UseLogsQueryParams = { limit?: number; @@ -24,7 +24,7 @@ export function useRatelimitLogsQuery({ const [realtimeLogsMap, setRealtimeLogsMap] = useState(() => new Map()); const [totalCount, setTotalCount] = useState(0); - const timestamp = useQueryTimestamp(); + const { queryTime: timestamp } = useQueryTime(); const { filters } = useFilters(); const queryClient = trpc.useUtils(); diff --git a/apps/dashboard/providers/query-time-provider.tsx b/apps/dashboard/providers/query-time-provider.tsx new file mode 100644 index 0000000000..4ce4acdeca --- /dev/null +++ b/apps/dashboard/providers/query-time-provider.tsx @@ -0,0 +1,46 @@ +"use client"; + +import { type ReactNode, createContext, useContext, useEffect, useState } from "react"; + +let queryTimestamp = Date.now(); +let subscribers: ((timestamp: number) => void)[] = []; + +export const refreshQueryTime = () => { + queryTimestamp = Date.now(); + subscribers.forEach((callback) => callback(queryTimestamp)); + return queryTimestamp; +}; + +type QueryTimeContextType = { + queryTime: number; + refreshQueryTime: () => number; +}; + +const QueryTimeContext = createContext(undefined); + +export const QueryTimeProvider = ({ children }: { children: ReactNode }) => { + const [queryTime, setQueryTime] = useState(queryTimestamp); + + useEffect(() => { + const callback = (newTimestamp: number) => setQueryTime(newTimestamp); + subscribers.push(callback); + + return () => { + subscribers = subscribers.filter((cb) => cb !== callback); + }; + }, []); + + return ( + + {children} + + ); +}; + +export const useQueryTime = () => { + const context = useContext(QueryTimeContext); + if (context === undefined) { + throw new Error("useQueryTime must be used within a QueryTimeProvider"); + } + return context; +}; From a6f35e07916f071c3b216bd942cedc0bc0277c42 Mon Sep 17 00:00:00 2001 From: ogzhanolguncu Date: Tue, 22 Apr 2025 17:21:33 +0300 Subject: [PATCH 3/5] chore: remove unused code --- .../logs/components/table/hooks/utils.ts | 27 ------------------- 1 file changed, 27 deletions(-) delete mode 100644 apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/table/hooks/utils.ts diff --git a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/table/hooks/utils.ts b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/table/hooks/utils.ts deleted file mode 100644 index 61ede24d07..0000000000 --- a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/components/table/hooks/utils.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { useEffect, useState } from "react"; - -let QUERY_TIMESTAMP = Date.now(); -let subscribers: ((timestamp: number) => void)[] = []; - -export const getQueryTimestamp = () => QUERY_TIMESTAMP; - -export const refreshQueryTimestamp = () => { - QUERY_TIMESTAMP = Date.now(); - subscribers.forEach((callback) => callback(QUERY_TIMESTAMP)); - return QUERY_TIMESTAMP; -}; - -export const useQueryTimestamp = () => { - const [timestamp, setTimestamp] = useState(QUERY_TIMESTAMP); - - useEffect(() => { - const callback = (newTimestamp: number) => setTimestamp(newTimestamp); - subscribers.push(callback); - - return () => { - subscribers = subscribers.filter((cb) => cb !== callback); - }; - }, []); - - return timestamp; -}; From d300803abb1a1c4402b6275c1920fafdf1833439 Mon Sep 17 00:00:00 2001 From: ogzhanolguncu Date: Tue, 22 Apr 2025 17:23:36 +0300 Subject: [PATCH 4/5] fix: add missing refresh --- .../_overview/components/controls/components/logs-refresh.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/_overview/components/controls/components/logs-refresh.tsx b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/_overview/components/controls/components/logs-refresh.tsx index 25e6f6dcdc..8ab9cd22b3 100644 --- a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/_overview/components/controls/components/logs-refresh.tsx +++ b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/_overview/components/controls/components/logs-refresh.tsx @@ -1,13 +1,17 @@ import { RefreshButton } from "@/components/logs/refresh-button"; import { trpc } from "@/lib/trpc/client"; +import { useQueryTime } from "@/providers/query-time-provider"; import { useFilters } from "../../../hooks/use-filters"; export const LogsRefresh = () => { const { filters } = useFilters(); + const { refreshQueryTime } = useQueryTime(); + const { ratelimit } = trpc.useUtils(); const hasRelativeFilter = filters.find((f) => f.field === "since"); const handleRefresh = () => { + refreshQueryTime(); ratelimit.overview.logs.query.invalidate(); ratelimit.overview.logs.queryRatelimitLatencyTimeseries.invalidate(); ratelimit.logs.queryRatelimitTimeseries.invalidate(); From f1072b01ea53a24de1008cfac19c2a896ce6150a Mon Sep 17 00:00:00 2001 From: ogzhanolguncu Date: Tue, 22 Apr 2025 17:40:39 +0300 Subject: [PATCH 5/5] docs: add jsdocs --- .../providers/query-time-provider.tsx | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/apps/dashboard/providers/query-time-provider.tsx b/apps/dashboard/providers/query-time-provider.tsx index 4ce4acdeca..2aa760ef0b 100644 --- a/apps/dashboard/providers/query-time-provider.tsx +++ b/apps/dashboard/providers/query-time-provider.tsx @@ -18,6 +18,40 @@ type QueryTimeContextType = { const QueryTimeContext = createContext(undefined); +/** + * Provides a shared timestamp reference for data fetching operations. + * + * Prevents React Query and other data fetching mechanisms from triggering + * unnecessary refetches due to components generating new timestamps on each render. + * + * Without this utility, each component would: + * - Create its own `Date.now()` when mounted or re-rendered + * - Cause React Query to treat it as a new dependency and refetch + * - Result in different components showing data from inconsistent time windows + * + * + * ```tsx + * // In a data fetching hook + * const { queryTime } = useQueryTime(); + * + * // Use in query parameters + * const params = { + * startTime: queryTime - TIME_WINDOW, + * endTime: queryTime + * }; + * + * // Only refetches when explicitly refreshed + * const { data } = useQuery(['data', params], fetchData); + * ``` + * + * When you need to refresh all queries simultaneously: + * ```tsx + * const { refreshQueryTime } = useQueryTime(); + * + * // Updates timestamp for ALL components + * const handleRefresh = () => refreshQueryTime(); + * ``` + */ export const QueryTimeProvider = ({ children }: { children: ReactNode }) => { const [queryTime, setQueryTime] = useState(queryTimestamp);