11import type { Edge } from 'reactflow'
22import { create } from 'zustand'
3- import { persist } from 'zustand/middleware'
3+ import { createJSONStorage , persist } from 'zustand/middleware'
44import { createLogger } from '@/lib/logs/console/logger'
55import type { BlockState } from '@/stores/workflows/workflow/types'
66import type {
@@ -14,11 +14,46 @@ import type {
1414
1515const logger = createLogger ( 'UndoRedoStore' )
1616const DEFAULT_CAPACITY = 100
17+ const MAX_STACKS = 5
1718
1819function 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+
2257function 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 ,
0 commit comments