From c4352185d64a9d5adc131a23e1a34eb1f60d3c98 Mon Sep 17 00:00:00 2001 From: Adrian Acala Date: Sat, 29 Nov 2025 11:35:57 -0800 Subject: [PATCH] refactor: optimize drawing data handling and cache management - Improve cache key generation using JSON.stringify for consistent formatting - Add promise deduplication in DrawingCard to prevent redundant API calls for full drawing data - Clear full data state when drawing ID changes to ensure fresh data loading - Fix async cache invalidation in drawing update and collection delete endpoints - Move cache invalidation after database operations in SQLite import endpoint - Add HydratedDrawingData type for better type safety in drawing data management --- backend/src/index.ts | 14 ++++---- frontend/src/components/DrawingCard.tsx | 44 +++++++++++++++++-------- 2 files changed, 39 insertions(+), 19 deletions(-) diff --git a/backend/src/index.ts b/backend/src/index.ts index 619053c..f078561 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -137,9 +137,11 @@ const buildDrawingsCacheKey = (keyParts: { collectionFilter: string; includeData: boolean; }) => - `${keyParts.searchTerm}|${keyParts.collectionFilter}|${ - keyParts.includeData ? "full" : "summary" - }`; + JSON.stringify([ + keyParts.searchTerm, + keyParts.collectionFilter, + keyParts.includeData ? "full" : "summary", + ]); const getCachedDrawingsBody = (key: string): Buffer | null => { const entry = drawingsCache.get(key); @@ -796,7 +798,7 @@ app.put("/drawings/:id", async (req, res) => { where: { id }, data, }); - invalidateDrawingsCache(); + await invalidateDrawingsCache(); console.log("[API] Update complete", { id, @@ -925,7 +927,7 @@ app.delete("/collections/:id", async (req, res) => { where: { id }, }), ]); - invalidateDrawingsCache(); + await invalidateDrawingsCache(); res.json({ success: true }); } catch (error) { @@ -1191,9 +1193,9 @@ app.post("/import/sqlite", upload.single("db"), async (req, res) => { // Reinitialize Prisma client await prisma.$disconnect(); + invalidateDrawingsCache(); res.json({ success: true, message: "Database imported successfully" }); - invalidateDrawingsCache(); } catch (error) { console.error(error); if (req.file) { diff --git a/frontend/src/components/DrawingCard.tsx b/frontend/src/components/DrawingCard.tsx index 31cea76..9e02e26 100644 --- a/frontend/src/components/DrawingCard.tsx +++ b/frontend/src/components/DrawingCard.tsx @@ -10,6 +10,12 @@ import { exportDrawingToFile } from '../utils/exportUtils'; import * as api from '../api'; +type HydratedDrawingData = { + elements: any[]; + appState: any; + files: Record; +}; + interface DrawingCardProps { drawing: DrawingSummary; collections: Collection[]; @@ -52,27 +58,39 @@ export const DrawingCard: React.FC = ({ const [previewSvg, setPreviewSvg] = useState(drawing.preview ?? null); const [contextMenu, setContextMenu] = useState<{ x: number; y: number } | null>(null); const [isExporting, setIsExporting] = useState(false); - const [fullData, setFullData] = useState<{ - elements: any[]; - appState: any; - files: Record | null; - } | null>(null); + const [fullData, setFullData] = useState(null); const fullDataRef = React.useRef(fullData); fullDataRef.current = fullData; + const fullDataPromiseRef = React.useRef | null>(null); + + useEffect(() => { + setFullData(null); + fullDataPromiseRef.current = null; + }, [drawing.id]); const ensureFullData = useCallback(async () => { if (fullDataRef.current) { return fullDataRef.current; } - const fullDrawing = await api.getDrawing(drawing.id); - const payload = { - elements: fullDrawing.elements || [], - appState: fullDrawing.appState || {}, - files: fullDrawing.files || {}, - }; - setFullData(payload); - return payload; + if (fullDataPromiseRef.current) { + return fullDataPromiseRef.current; + } + const promise = api.getDrawing(drawing.id).then((fullDrawing) => { + const payload: HydratedDrawingData = { + elements: fullDrawing.elements || [], + appState: fullDrawing.appState || {}, + files: fullDrawing.files || {}, + }; + setFullData(payload); + fullDataPromiseRef.current = null; + return payload; + }).catch((error) => { + fullDataPromiseRef.current = null; + throw error; + }); + fullDataPromiseRef.current = promise; + return promise; }, [drawing.id]); useEffect(() => {