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
This commit is contained in:
Adrian Acala
2025-11-29 11:35:57 -08:00
parent 6f050aec7d
commit c4352185d6
2 changed files with 39 additions and 19 deletions
+8 -6
View File
@@ -137,9 +137,11 @@ const buildDrawingsCacheKey = (keyParts: {
collectionFilter: string; collectionFilter: string;
includeData: boolean; includeData: boolean;
}) => }) =>
`${keyParts.searchTerm}|${keyParts.collectionFilter}|${ JSON.stringify([
keyParts.includeData ? "full" : "summary" keyParts.searchTerm,
}`; keyParts.collectionFilter,
keyParts.includeData ? "full" : "summary",
]);
const getCachedDrawingsBody = (key: string): Buffer | null => { const getCachedDrawingsBody = (key: string): Buffer | null => {
const entry = drawingsCache.get(key); const entry = drawingsCache.get(key);
@@ -796,7 +798,7 @@ app.put("/drawings/:id", async (req, res) => {
where: { id }, where: { id },
data, data,
}); });
invalidateDrawingsCache(); await invalidateDrawingsCache();
console.log("[API] Update complete", { console.log("[API] Update complete", {
id, id,
@@ -925,7 +927,7 @@ app.delete("/collections/:id", async (req, res) => {
where: { id }, where: { id },
}), }),
]); ]);
invalidateDrawingsCache(); await invalidateDrawingsCache();
res.json({ success: true }); res.json({ success: true });
} catch (error) { } catch (error) {
@@ -1191,9 +1193,9 @@ app.post("/import/sqlite", upload.single("db"), async (req, res) => {
// Reinitialize Prisma client // Reinitialize Prisma client
await prisma.$disconnect(); await prisma.$disconnect();
invalidateDrawingsCache();
res.json({ success: true, message: "Database imported successfully" }); res.json({ success: true, message: "Database imported successfully" });
invalidateDrawingsCache();
} catch (error) { } catch (error) {
console.error(error); console.error(error);
if (req.file) { if (req.file) {
+31 -13
View File
@@ -10,6 +10,12 @@ import { exportDrawingToFile } from '../utils/exportUtils';
import * as api from '../api'; import * as api from '../api';
type HydratedDrawingData = {
elements: any[];
appState: any;
files: Record<string, any>;
};
interface DrawingCardProps { interface DrawingCardProps {
drawing: DrawingSummary; drawing: DrawingSummary;
collections: Collection[]; collections: Collection[];
@@ -52,27 +58,39 @@ export const DrawingCard: React.FC<DrawingCardProps> = ({
const [previewSvg, setPreviewSvg] = useState<string | null>(drawing.preview ?? null); const [previewSvg, setPreviewSvg] = useState<string | null>(drawing.preview ?? null);
const [contextMenu, setContextMenu] = useState<{ x: number; y: number } | null>(null); const [contextMenu, setContextMenu] = useState<{ x: number; y: number } | null>(null);
const [isExporting, setIsExporting] = useState(false); const [isExporting, setIsExporting] = useState(false);
const [fullData, setFullData] = useState<{ const [fullData, setFullData] = useState<HydratedDrawingData | null>(null);
elements: any[];
appState: any;
files: Record<string, any> | null;
} | null>(null);
const fullDataRef = React.useRef(fullData); const fullDataRef = React.useRef(fullData);
fullDataRef.current = fullData; fullDataRef.current = fullData;
const fullDataPromiseRef = React.useRef<Promise<HydratedDrawingData> | null>(null);
useEffect(() => {
setFullData(null);
fullDataPromiseRef.current = null;
}, [drawing.id]);
const ensureFullData = useCallback(async () => { const ensureFullData = useCallback(async () => {
if (fullDataRef.current) { if (fullDataRef.current) {
return fullDataRef.current; return fullDataRef.current;
} }
const fullDrawing = await api.getDrawing(drawing.id); if (fullDataPromiseRef.current) {
const payload = { return fullDataPromiseRef.current;
elements: fullDrawing.elements || [], }
appState: fullDrawing.appState || {}, const promise = api.getDrawing(drawing.id).then((fullDrawing) => {
files: fullDrawing.files || {}, const payload: HydratedDrawingData = {
}; elements: fullDrawing.elements || [],
setFullData(payload); appState: fullDrawing.appState || {},
return payload; files: fullDrawing.files || {},
};
setFullData(payload);
fullDataPromiseRef.current = null;
return payload;
}).catch((error) => {
fullDataPromiseRef.current = null;
throw error;
});
fullDataPromiseRef.current = promise;
return promise;
}, [drawing.id]); }, [drawing.id]);
useEffect(() => { useEffect(() => {