"use client"; import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, type ReactNode, } from "react"; import { useConvex, useConvexAuth, useMutation } from "convex/react"; import { api } from "@/convex/_generated/api"; import type { Doc } from "@/convex/_generated/dataModel"; type AdjustmentPresetDoc = Doc<"adjustmentPresets">; type PresetsByNodeType = Map; type SaveAdjustmentPresetArgs = { name: string; nodeType: AdjustmentPresetDoc["nodeType"]; params: unknown; }; type CanvasPresetsContextValue = { presetsByNodeType: PresetsByNodeType; savePreset: (args: SaveAdjustmentPresetArgs) => Promise; }; const MAX_AUTO_LOAD_ATTEMPTS = 2; const RETRY_DELAY_MS = 1000; const CanvasPresetsContext = createContext(null); function groupPresetsByNodeType(rawPresets: AdjustmentPresetDoc[]): PresetsByNodeType { const next = new Map(); for (const preset of rawPresets) { const existing = next.get(preset.nodeType); if (existing) { existing.push(preset); } else { next.set(preset.nodeType, [preset]); } } return next; } type CanvasPresetsProviderProps = { enabled?: boolean; children: ReactNode; }; export function CanvasPresetsProvider({ enabled = true, children, }: CanvasPresetsProviderProps) { const convex = useConvex(); const { isAuthenticated } = useConvexAuth(); const savePresetMutation = useMutation(api.presets.save); const latestLoadRequestIdRef = useRef(0); const autoLoadInFlightRef = useRef(false); const retryTimeoutRef = useRef | null>(null); const [rawPresets, setRawPresets] = useState([]); const [hasLoadedOnce, setHasLoadedOnce] = useState(false); const [retryNonce, setRetryNonce] = useState(0); const clearScheduledRetry = useCallback(() => { if (retryTimeoutRef.current !== null) { clearTimeout(retryTimeoutRef.current); retryTimeoutRef.current = null; } }, []); const scheduleRetry = useCallback(() => { if (retryTimeoutRef.current !== null) { return; } retryTimeoutRef.current = setTimeout(() => { retryTimeoutRef.current = null; setRetryNonce((current) => current + 1); }, RETRY_DELAY_MS); }, []); const loadPresets = useCallback(async () => { const presets = await convex.query(api.presets.list, {}); return presets as AdjustmentPresetDoc[]; }, [convex]); const refreshPresets = useCallback(async () => { const requestId = ++latestLoadRequestIdRef.current; try { const presets = await loadPresets(); if (requestId === latestLoadRequestIdRef.current) { clearScheduledRetry(); setRawPresets(presets); } return { requestId, presets }; } catch (error: unknown) { throw { requestId, error }; } }, [clearScheduledRetry, loadPresets]); useEffect(() => { return () => { clearScheduledRetry(); }; }, [clearScheduledRetry]); useEffect(() => { if ( !enabled || !isAuthenticated || hasLoadedOnce || autoLoadInFlightRef.current ) { return; } let cancelled = false; autoLoadInFlightRef.current = true; void (async () => { for (let attempt = 0; attempt < MAX_AUTO_LOAD_ATTEMPTS; attempt += 1) { try { await refreshPresets(); if (!cancelled) { setHasLoadedOnce(true); } return; } catch (caught: unknown) { if (cancelled) { return; } const refreshError = typeof caught === "object" && caught !== null && "error" in caught ? ((caught as { error: unknown }).error ?? caught) : caught; console.warn("[CanvasPresetsProvider] failed to load presets", { message: refreshError instanceof Error ? refreshError.message : String(refreshError), attempt: attempt + 1, }); } } if (!cancelled) { scheduleRetry(); } })().finally(() => { autoLoadInFlightRef.current = false; }); return () => { cancelled = true; }; }, [enabled, hasLoadedOnce, isAuthenticated, refreshPresets, retryNonce, scheduleRetry]); const savePreset = useCallback( async (args: SaveAdjustmentPresetArgs) => { await savePresetMutation(args); try { await refreshPresets(); setHasLoadedOnce(true); } catch (caught: unknown) { const refreshError = typeof caught === "object" && caught !== null && "error" in caught ? ((caught as { error: unknown }).error ?? caught) : caught; const requestId = typeof caught === "object" && caught !== null && "requestId" in caught ? ((caught as { requestId: unknown }).requestId as number) : latestLoadRequestIdRef.current; console.warn("[CanvasPresetsProvider] failed to refresh presets after save", { message: refreshError instanceof Error ? refreshError.message : String(refreshError), }); if (requestId === latestLoadRequestIdRef.current) { setHasLoadedOnce(false); scheduleRetry(); } } }, [refreshPresets, savePresetMutation, scheduleRetry], ); const presetsByNodeType = useMemo( () => groupPresetsByNodeType(rawPresets), [rawPresets], ); const contextValue = useMemo( () => ({ presetsByNodeType, savePreset, }), [presetsByNodeType, savePreset], ); return ( {children} ); } export function useCanvasAdjustmentPresets( nodeType: AdjustmentPresetDoc["nodeType"], ): AdjustmentPresetDoc[] { const context = useContext(CanvasPresetsContext); if (context === null) { return []; } return context.presetsByNodeType.get(nodeType) ?? []; } export function useSaveCanvasAdjustmentPreset(): CanvasPresetsContextValue["savePreset"] { const context = useContext(CanvasPresetsContext); if (context === null) { throw new Error("useSaveCanvasAdjustmentPreset must be used within CanvasPresetsProvider"); } return context.savePreset; }