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
This commit is contained in:
Vikhyath Mondreti
2025-11-20 12:12:04 -08:00
committed by GitHub
parent f208ff9356
commit d7586cdd9f
3 changed files with 88 additions and 19 deletions

View File

@@ -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 {
<Dialog open={open} onOpenChange={handleClose}>
<DialogContent
className='flex h-[80vh] w-full max-w-[840px] flex-col gap-0 p-0'
style={{ zIndex: 99999999 }}
style={{ zIndex: 10000050 }}
hideCloseButton
onKeyDown={(e) => {
if (e.key === 'Escape' && (showEnvVars || showTags || showSchemaParams)) {

View File

@@ -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<string, BlockState>; edgesById: Record<string, Edge> }
@@ -73,7 +108,28 @@ export const useUndoRedoStore = create<UndoRedoState>()(
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<UndoRedoState>()(
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<UndoRedoState>()(
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<UndoRedoState>()(
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<UndoRedoState>()(
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<UndoRedoState>()(
newStacks[key] = {
undo: stack.undo.slice(-capacity),
redo: stack.redo.slice(-capacity),
lastUpdated: stack.lastUpdated,
}
}
@@ -330,7 +397,7 @@ export const useUndoRedoStore = create<UndoRedoState>()(
set({
stacks: {
...state.stacks,
[key]: { undo: validUndo, redo: validRedo },
[key]: { ...stack, undo: validUndo, redo: validRedo },
},
})
@@ -347,6 +414,7 @@ export const useUndoRedoStore = create<UndoRedoState>()(
}),
{
name: 'workflow-undo-redo',
storage: createJSONStorage(() => safeStorageAdapter),
partialize: (state) => ({
stacks: state.stacks,
capacity: state.capacity,

View File

@@ -147,6 +147,7 @@ export interface UndoRedoState {
{
undo: OperationEntry[]
redo: OperationEntry[]
lastUpdated?: number
}
>
capacity: number