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.id = styleId
styleEl.textContent = ` styleEl.textContent = `
[data-radix-portal] [data-radix-dialog-overlay] { [data-radix-portal] [data-radix-dialog-overlay] {
z-index: 99999998 !important; z-index: 10000048 !important;
} }
` `
document.head.appendChild(styleEl) document.head.appendChild(styleEl)
@@ -934,7 +934,7 @@ try {
<Dialog open={open} onOpenChange={handleClose}> <Dialog open={open} onOpenChange={handleClose}>
<DialogContent <DialogContent
className='flex h-[80vh] w-full max-w-[840px] flex-col gap-0 p-0' className='flex h-[80vh] w-full max-w-[840px] flex-col gap-0 p-0'
style={{ zIndex: 99999999 }} style={{ zIndex: 10000050 }}
hideCloseButton hideCloseButton
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === 'Escape' && (showEnvVars || showTags || showSchemaParams)) { if (e.key === 'Escape' && (showEnvVars || showTags || showSchemaParams)) {

View File

@@ -1,6 +1,6 @@
import type { Edge } from 'reactflow' import type { Edge } from 'reactflow'
import { create } from 'zustand' import { create } from 'zustand'
import { persist } from 'zustand/middleware' import { createJSONStorage, persist } from 'zustand/middleware'
import { createLogger } from '@/lib/logs/console/logger' import { createLogger } from '@/lib/logs/console/logger'
import type { BlockState } from '@/stores/workflows/workflow/types' import type { BlockState } from '@/stores/workflows/workflow/types'
import type { import type {
@@ -14,11 +14,46 @@ import type {
const logger = createLogger('UndoRedoStore') const logger = createLogger('UndoRedoStore')
const DEFAULT_CAPACITY = 100 const DEFAULT_CAPACITY = 100
const MAX_STACKS = 5
function getStackKey(workflowId: string, userId: string): string { function getStackKey(workflowId: string, userId: string): string {
return `${workflowId}:${userId}` 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( function isOperationApplicable(
operation: Operation, operation: Operation,
graph: { blocksById: Record<string, BlockState>; edgesById: Record<string, Edge> } 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) => { push: (workflowId: string, userId: string, entry: OperationEntry) => {
const key = getStackKey(workflowId, userId) const key = getStackKey(workflowId, userId)
const state = get() 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 // Coalesce consecutive move-block operations for the same block
if (entry.operation.type === 'move-block') { if (entry.operation.type === 'move-block') {
@@ -137,12 +193,13 @@ export const useUndoRedoStore = create<UndoRedoState>()(
return [...stack.undo.slice(0, -1), newEntry] return [...stack.undo.slice(0, -1), newEntry]
})() })()
set({ currentStacks[key] = {
stacks: { undo: newUndoCoalesced,
...state.stacks, redo: [],
[key]: { undo: newUndoCoalesced, redo: [] }, lastUpdated: Date.now(),
}, }
})
set({ stacks: currentStacks })
logger.debug('Coalesced consecutive move operations', { logger.debug('Coalesced consecutive move operations', {
workflowId, workflowId,
@@ -160,12 +217,13 @@ export const useUndoRedoStore = create<UndoRedoState>()(
newUndo.shift() newUndo.shift()
} }
set({ currentStacks[key] = {
stacks: { undo: newUndo,
...state.stacks, redo: [],
[key]: { undo: newUndo, redo: [] }, lastUpdated: Date.now(),
}, }
})
set({ stacks: currentStacks })
logger.debug('Pushed operation to undo stack', { logger.debug('Pushed operation to undo stack', {
workflowId, workflowId,
@@ -195,7 +253,11 @@ export const useUndoRedoStore = create<UndoRedoState>()(
set({ set({
stacks: { stacks: {
...state.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({ set({
stacks: { stacks: {
...state.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] = { newStacks[key] = {
undo: stack.undo.slice(-capacity), undo: stack.undo.slice(-capacity),
redo: stack.redo.slice(-capacity), redo: stack.redo.slice(-capacity),
lastUpdated: stack.lastUpdated,
} }
} }
@@ -330,7 +397,7 @@ export const useUndoRedoStore = create<UndoRedoState>()(
set({ set({
stacks: { stacks: {
...state.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', name: 'workflow-undo-redo',
storage: createJSONStorage(() => safeStorageAdapter),
partialize: (state) => ({ partialize: (state) => ({
stacks: state.stacks, stacks: state.stacks,
capacity: state.capacity, capacity: state.capacity,

View File

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