mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-10 07:27:57 -05:00
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:
committed by
GitHub
parent
f208ff9356
commit
d7586cdd9f
@@ -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)) {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -147,6 +147,7 @@ export interface UndoRedoState {
|
|||||||
{
|
{
|
||||||
undo: OperationEntry[]
|
undo: OperationEntry[]
|
||||||
redo: OperationEntry[]
|
redo: OperationEntry[]
|
||||||
|
lastUpdated?: number
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
capacity: number
|
capacity: number
|
||||||
|
|||||||
Reference in New Issue
Block a user