Skip to main content
Glama

Convex MCP server

Official
by get-convex
usePausedLiveData.ts6.18 kB
import { ConvexReactClient } from "convex/react"; import { useCallback, useState, useRef, useEffect, useMemo, useContext, } from "react"; import { useMount } from "react-use"; import { DeploymentInfoContext } from "@common/lib/deploymentContext"; import { useGlobalLocalStorage } from "@common/lib/useGlobalLocalStorage"; import { useDeploymentUrl, useAdminKey } from "@common/lib/deploymentApi"; import { toast } from "@common/lib/utils"; import type { FunctionReference } from "convex/server"; const RATE_LIMIT_BUCKET_MS = 10 * 1000; // > 10 updates per second const RATE_LIMIT_THRESHOLD = 10 * 10; /** * Hook to handle rate limiting for live data */ function useRateLimitChanges<T>( items: T[], isPaused: boolean, onRateLimited: () => void, ) { const callCountRef = useRef(0); const lastResetRef = useRef(Date.now()); const [isRateLimited, setIsRateLimited] = useState(false); useEffect(() => { if (isRateLimited || isPaused) { return; } const now = Date.now(); if (now - lastResetRef.current > RATE_LIMIT_BUCKET_MS) { callCountRef.current = 0; lastResetRef.current = now; } callCountRef.current += 1; if (callCountRef.current > RATE_LIMIT_THRESHOLD) { setIsRateLimited(true); onRateLimited(); } }, [items, isPaused, isRateLimited, onRateLimited]); return isRateLimited; } /** * Hook to manage paused state for live data * @param results - The current results from the live query * @param args - Arguments for the query * @param storageKey - Key suffix to use for localStorage * @param udfName - Name or reference of the UDF to call when loading paused data * @param numItems - Number of items to fetch per page */ export function usePausedLiveData<TResult, TArgs>({ results, args, storageKey, udfName, numItems = 50, }: { results: TResult[]; args: TArgs; storageKey: string; udfName: string | FunctionReference<"query">; numItems?: number; }) { const [pausedData, setPausedData] = useState<TResult[]>(results); const [lastCursor, setLastCursor] = useState<string | null>(null); const [canLoadMore, setCanLoadMore] = useState(true); const [isLoadingMore, setIsLoadingMore] = useState(false); const currentArgsRef = useRef(args); const { useCurrentDeployment } = useContext(DeploymentInfoContext); const deployment = useCurrentDeployment(); // Store the paused state in local storage so it persists across refreshes const [isPaused, setIsPaused] = useGlobalLocalStorage( `${deployment?.name}/${storageKey}`, false, ); const onRateLimited = () => { // When we get rate limited, we should immediately pause setIsPaused(true); // Store the current result set from the live query so we can show it when paused setPausedData(results); toast( "error", `There are too many updates to show live. Updates have automatically been paused.`, "liveUpdatesPaused", ); }; const isRateLimited = useRateLimitChanges(results, isPaused, onRateLimited); const [isLoadingPausedData, setIsLoadingPausedData] = useState(false); const deploymentUrl = useDeploymentUrl(); const adminKey = useAdminKey(); // Create convex client for making one-off queries const client = useMemo(() => { const c = new ConvexReactClient(deploymentUrl, { reportDebugInfoToConvex: true, }); c.setAdminAuth(adminKey); return c; }, [adminKey, deploymentUrl]); // Helper function to query with proper typing const queryWithArgs = useCallback( async (cursor: string | null, id: number) => { // Handle both string and FunctionReference types const result = typeof udfName === "string" ? await (client as any).query(udfName, { ...args, paginationOpts: { numItems, cursor, id, }, }) : await client.query(udfName, { ...args, paginationOpts: { numItems, cursor, id, }, }); return result; }, [args, client, numItems, udfName], ); const loadFirstPage = useCallback(async () => { setIsLoadingPausedData(true); try { // Fetch one page const result = await queryWithArgs(null, 0); setPausedData(result.page); setLastCursor(result.continueCursor); setCanLoadMore(!result.isDone); } catch (e) { toast("error", "Failed to load data", "loadData"); } finally { setIsLoadingPausedData(false); } }, [queryWithArgs]); const loadMorePaused = useCallback(async () => { if (!canLoadMore || isLoadingMore) return; setIsLoadingMore(true); try { const result = await queryWithArgs(lastCursor, pausedData.length); setPausedData((prev) => [...prev, ...result.page]); setLastCursor(result.continueCursor); setCanLoadMore(!result.isDone); } catch (e) { toast("error", "Failed to load more data", "loadMoreData"); } finally { setIsLoadingMore(false); } }, [ canLoadMore, isLoadingMore, lastCursor, pausedData.length, queryWithArgs, ]); useMount(() => { isPaused && void loadFirstPage(); }); // Check if args have changed, and if isPaused, reload the data useEffect(() => { // Check if there are meaningful differences in the args const argsChanged = JSON.stringify(args) !== JSON.stringify(currentArgsRef.current); if (argsChanged) { currentArgsRef.current = args; // If we're paused and args changed, reload the data with the new args if (isPaused && !isLoadingPausedData) { void loadFirstPage(); } } }, [args, isPaused, isLoadingPausedData, loadFirstPage]); return { pausedData, isLoadingPausedData, isLoadingMore, canLoadMore, isRateLimited, togglePaused: useCallback(() => { setPausedData(results); setLastCursor(null); setCanLoadMore(true); setIsPaused(!isPaused); }, [results, setIsPaused, isPaused]), reload: loadFirstPage, loadMorePaused, }; }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/get-convex/convex-backend'

If you have feedback or need assistance with the MCP directory API, please join our Discord server