From d7586cdd9fe76f7cdbd910ccd10ce968daa3c150 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Thu, 20 Nov 2025 12:12:04 -0800 Subject: [PATCH] fix(undo-redo): eviction policy to not have unbounded undo-redo stacks (#2079) * fix(undo-redo): eviction policy to not have unbounded undo-redo stacks * fix zindex custom tools delete --- .../custom-tool-modal/custom-tool-modal.tsx | 4 +- apps/sim/stores/undo-redo/store.ts | 102 +++++++++++++++--- apps/sim/stores/undo-redo/types.ts | 1 + 3 files changed, 88 insertions(+), 19 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tool-input/components/custom-tool-modal/custom-tool-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tool-input/components/custom-tool-modal/custom-tool-modal.tsx index f4af7df5f..0219f575b 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tool-input/components/custom-tool-modal/custom-tool-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tool-input/components/custom-tool-modal/custom-tool-modal.tsx @@ -915,7 +915,7 @@ try { styleEl.id = styleId styleEl.textContent = ` [data-radix-portal] [data-radix-dialog-overlay] { - z-index: 99999998 !important; + z-index: 10000048 !important; } ` document.head.appendChild(styleEl) @@ -934,7 +934,7 @@ try { { if (e.key === 'Escape' && (showEnvVars || showTags || showSchemaParams)) { diff --git a/apps/sim/stores/undo-redo/store.ts b/apps/sim/stores/undo-redo/store.ts index c57afbe2c..80e836155 100644 --- a/apps/sim/stores/undo-redo/store.ts +++ b/apps/sim/stores/undo-redo/store.ts @@ -1,6 +1,6 @@ import type { Edge } from 'reactflow' import { create } from 'zustand' -import { persist } from 'zustand/middleware' +import { createJSONStorage, persist } from 'zustand/middleware' import { createLogger } from '@/lib/logs/console/logger' import type { BlockState } from '@/stores/workflows/workflow/types' import type { @@ -14,11 +14,46 @@ import type { const logger = createLogger('UndoRedoStore') const DEFAULT_CAPACITY = 100 +const MAX_STACKS = 5 function getStackKey(workflowId: string, userId: string): string { return `${workflowId}:${userId}` } +/** + * Custom storage adapter for Zustand's persist middleware. + * We need this wrapper to gracefully handle 'QuotaExceededError' when localStorage is full. + * Without this, the default storage engine would throw and crash the application. + */ +const safeStorageAdapter = { + getItem: (name: string): string | null => { + if (typeof localStorage === 'undefined') return null + try { + return localStorage.getItem(name) + } catch (e) { + logger.warn('Failed to read from localStorage', e) + return null + } + }, + setItem: (name: string, value: string): void => { + if (typeof localStorage === 'undefined') return + try { + localStorage.setItem(name, value) + } catch (e) { + // Log warning but don't crash - this handles QuotaExceededError + logger.warn('Failed to save to localStorage', e) + } + }, + removeItem: (name: string): void => { + if (typeof localStorage === 'undefined') return + try { + localStorage.removeItem(name) + } catch (e) { + logger.warn('Failed to remove from localStorage', e) + } + }, +} + function isOperationApplicable( operation: Operation, graph: { blocksById: Record; edgesById: Record } @@ -73,7 +108,28 @@ export const useUndoRedoStore = create()( push: (workflowId: string, userId: string, entry: OperationEntry) => { const key = getStackKey(workflowId, userId) const state = get() - const stack = state.stacks[key] || { undo: [], redo: [] } + const currentStacks = { ...state.stacks } + + // Limit number of stacks + const stackKeys = Object.keys(currentStacks) + if (stackKeys.length >= MAX_STACKS && !currentStacks[key]) { + let oldestKey: string | null = null + let oldestTime = Number.POSITIVE_INFINITY + + for (const k of stackKeys) { + const t = currentStacks[k].lastUpdated ?? 0 + if (t < oldestTime) { + oldestTime = t + oldestKey = k + } + } + + if (oldestKey) { + delete currentStacks[oldestKey] + } + } + + const stack = currentStacks[key] || { undo: [], redo: [] } // Coalesce consecutive move-block operations for the same block if (entry.operation.type === 'move-block') { @@ -137,12 +193,13 @@ export const useUndoRedoStore = create()( return [...stack.undo.slice(0, -1), newEntry] })() - set({ - stacks: { - ...state.stacks, - [key]: { undo: newUndoCoalesced, redo: [] }, - }, - }) + currentStacks[key] = { + undo: newUndoCoalesced, + redo: [], + lastUpdated: Date.now(), + } + + set({ stacks: currentStacks }) logger.debug('Coalesced consecutive move operations', { workflowId, @@ -160,12 +217,13 @@ export const useUndoRedoStore = create()( newUndo.shift() } - set({ - stacks: { - ...state.stacks, - [key]: { undo: newUndo, redo: [] }, - }, - }) + currentStacks[key] = { + undo: newUndo, + redo: [], + lastUpdated: Date.now(), + } + + set({ stacks: currentStacks }) logger.debug('Pushed operation to undo stack', { workflowId, @@ -195,7 +253,11 @@ export const useUndoRedoStore = create()( set({ stacks: { ...state.stacks, - [key]: { undo: newUndo, redo: newRedo }, + [key]: { + undo: newUndo, + redo: newRedo, + lastUpdated: Date.now(), + }, }, }) @@ -230,7 +292,11 @@ export const useUndoRedoStore = create()( set({ stacks: { ...state.stacks, - [key]: { undo: newUndo, redo: newRedo }, + [key]: { + undo: newUndo, + redo: newRedo, + lastUpdated: Date.now(), + }, }, }) @@ -295,6 +361,7 @@ export const useUndoRedoStore = create()( newStacks[key] = { undo: stack.undo.slice(-capacity), redo: stack.redo.slice(-capacity), + lastUpdated: stack.lastUpdated, } } @@ -330,7 +397,7 @@ export const useUndoRedoStore = create()( set({ stacks: { ...state.stacks, - [key]: { undo: validUndo, redo: validRedo }, + [key]: { ...stack, undo: validUndo, redo: validRedo }, }, }) @@ -347,6 +414,7 @@ export const useUndoRedoStore = create()( }), { name: 'workflow-undo-redo', + storage: createJSONStorage(() => safeStorageAdapter), partialize: (state) => ({ stacks: state.stacks, capacity: state.capacity, diff --git a/apps/sim/stores/undo-redo/types.ts b/apps/sim/stores/undo-redo/types.ts index 16f5fbd35..f635deba3 100644 --- a/apps/sim/stores/undo-redo/types.ts +++ b/apps/sim/stores/undo-redo/types.ts @@ -147,6 +147,7 @@ export interface UndoRedoState { { undo: OperationEntry[] redo: OperationEntry[] + lastUpdated?: number } > capacity: number