Skip to content

Commit d7586cd

Browse files
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
1 parent f208ff9 commit d7586cd

3 files changed

Lines changed: 88 additions & 19 deletions

File tree

  • apps/sim
    • app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tool-input/components/custom-tool-modal
    • stores/undo-redo

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -915,7 +915,7 @@ try {
915915
styleEl.id = styleId
916916
styleEl.textContent = `
917917
[data-radix-portal] [data-radix-dialog-overlay] {
918-
z-index: 99999998 !important;
918+
z-index: 10000048 !important;
919919
}
920920
`
921921
document.head.appendChild(styleEl)
@@ -934,7 +934,7 @@ try {
934934
<Dialog open={open} onOpenChange={handleClose}>
935935
<DialogContent
936936
className='flex h-[80vh] w-full max-w-[840px] flex-col gap-0 p-0'
937-
style={{ zIndex: 99999999 }}
937+
style={{ zIndex: 10000050 }}
938938
hideCloseButton
939939
onKeyDown={(e) => {
940940
if (e.key === 'Escape' && (showEnvVars || showTags || showSchemaParams)) {

apps/sim/stores/undo-redo/store.ts

Lines changed: 85 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { Edge } from 'reactflow'
22
import { create } from 'zustand'
3-
import { persist } from 'zustand/middleware'
3+
import { createJSONStorage, persist } from 'zustand/middleware'
44
import { createLogger } from '@/lib/logs/console/logger'
55
import type { BlockState } from '@/stores/workflows/workflow/types'
66
import type {
@@ -14,11 +14,46 @@ import type {
1414

1515
const logger = createLogger('UndoRedoStore')
1616
const DEFAULT_CAPACITY = 100
17+
const MAX_STACKS = 5
1718

1819
function getStackKey(workflowId: string, userId: string): string {
1920
return `${workflowId}:${userId}`
2021
}
2122

23+
/**
24+
* Custom storage adapter for Zustand's persist middleware.
25+
* We need this wrapper to gracefully handle 'QuotaExceededError' when localStorage is full.
26+
* Without this, the default storage engine would throw and crash the application.
27+
*/
28+
const safeStorageAdapter = {
29+
getItem: (name: string): string | null => {
30+
if (typeof localStorage === 'undefined') return null
31+
try {
32+
return localStorage.getItem(name)
33+
} catch (e) {
34+
logger.warn('Failed to read from localStorage', e)
35+
return null
36+
}
37+
},
38+
setItem: (name: string, value: string): void => {
39+
if (typeof localStorage === 'undefined') return
40+
try {
41+
localStorage.setItem(name, value)
42+
} catch (e) {
43+
// Log warning but don't crash - this handles QuotaExceededError
44+
logger.warn('Failed to save to localStorage', e)
45+
}
46+
},
47+
removeItem: (name: string): void => {
48+
if (typeof localStorage === 'undefined') return
49+
try {
50+
localStorage.removeItem(name)
51+
} catch (e) {
52+
logger.warn('Failed to remove from localStorage', e)
53+
}
54+
},
55+
}
56+
2257
function isOperationApplicable(
2358
operation: Operation,
2459
graph: { blocksById: Record<string, BlockState>; edgesById: Record<string, Edge> }
@@ -73,7 +108,28 @@ export const useUndoRedoStore = create<UndoRedoState>()(
73108
push: (workflowId: string, userId: string, entry: OperationEntry) => {
74109
const key = getStackKey(workflowId, userId)
75110
const state = get()
76-
const stack = state.stacks[key] || { undo: [], redo: [] }
111+
const currentStacks = { ...state.stacks }
112+
113+
// Limit number of stacks
114+
const stackKeys = Object.keys(currentStacks)
115+
if (stackKeys.length >= MAX_STACKS && !currentStacks[key]) {
116+
let oldestKey: string | null = null
117+
let oldestTime = Number.POSITIVE_INFINITY
118+
119+
for (const k of stackKeys) {
120+
const t = currentStacks[k].lastUpdated ?? 0
121+
if (t < oldestTime) {
122+
oldestTime = t
123+
oldestKey = k
124+
}
125+
}
126+
127+
if (oldestKey) {
128+
delete currentStacks[oldestKey]
129+
}
130+
}
131+
132+
const stack = currentStacks[key] || { undo: [], redo: [] }
77133

78134
// Coalesce consecutive move-block operations for the same block
79135
if (entry.operation.type === 'move-block') {
@@ -137,12 +193,13 @@ export const useUndoRedoStore = create<UndoRedoState>()(
137193
return [...stack.undo.slice(0, -1), newEntry]
138194
})()
139195

140-
set({
141-
stacks: {
142-
...state.stacks,
143-
[key]: { undo: newUndoCoalesced, redo: [] },
144-
},
145-
})
196+
currentStacks[key] = {
197+
undo: newUndoCoalesced,
198+
redo: [],
199+
lastUpdated: Date.now(),
200+
}
201+
202+
set({ stacks: currentStacks })
146203

147204
logger.debug('Coalesced consecutive move operations', {
148205
workflowId,
@@ -160,12 +217,13 @@ export const useUndoRedoStore = create<UndoRedoState>()(
160217
newUndo.shift()
161218
}
162219

163-
set({
164-
stacks: {
165-
...state.stacks,
166-
[key]: { undo: newUndo, redo: [] },
167-
},
168-
})
220+
currentStacks[key] = {
221+
undo: newUndo,
222+
redo: [],
223+
lastUpdated: Date.now(),
224+
}
225+
226+
set({ stacks: currentStacks })
169227

170228
logger.debug('Pushed operation to undo stack', {
171229
workflowId,
@@ -195,7 +253,11 @@ export const useUndoRedoStore = create<UndoRedoState>()(
195253
set({
196254
stacks: {
197255
...state.stacks,
198-
[key]: { undo: newUndo, redo: newRedo },
256+
[key]: {
257+
undo: newUndo,
258+
redo: newRedo,
259+
lastUpdated: Date.now(),
260+
},
199261
},
200262
})
201263

@@ -230,7 +292,11 @@ export const useUndoRedoStore = create<UndoRedoState>()(
230292
set({
231293
stacks: {
232294
...state.stacks,
233-
[key]: { undo: newUndo, redo: newRedo },
295+
[key]: {
296+
undo: newUndo,
297+
redo: newRedo,
298+
lastUpdated: Date.now(),
299+
},
234300
},
235301
})
236302

@@ -295,6 +361,7 @@ export const useUndoRedoStore = create<UndoRedoState>()(
295361
newStacks[key] = {
296362
undo: stack.undo.slice(-capacity),
297363
redo: stack.redo.slice(-capacity),
364+
lastUpdated: stack.lastUpdated,
298365
}
299366
}
300367

@@ -330,7 +397,7 @@ export const useUndoRedoStore = create<UndoRedoState>()(
330397
set({
331398
stacks: {
332399
...state.stacks,
333-
[key]: { undo: validUndo, redo: validRedo },
400+
[key]: { ...stack, undo: validUndo, redo: validRedo },
334401
},
335402
})
336403

@@ -347,6 +414,7 @@ export const useUndoRedoStore = create<UndoRedoState>()(
347414
}),
348415
{
349416
name: 'workflow-undo-redo',
417+
storage: createJSONStorage(() => safeStorageAdapter),
350418
partialize: (state) => ({
351419
stacks: state.stacks,
352420
capacity: state.capacity,

apps/sim/stores/undo-redo/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ export interface UndoRedoState {
147147
{
148148
undo: OperationEntry[]
149149
redo: OperationEntry[]
150+
lastUpdated?: number
150151
}
151152
>
152153
capacity: number

0 commit comments

Comments
 (0)