1+ // State debugging and monitoring utilities
2+
3+ // Interface for state change logging
4+ export interface StateLogEntry {
5+ timestamp : number ;
6+ storeName : string ;
7+ action : string ;
8+ prevState : any ;
9+ nextState : any ;
10+ payload ?: any ;
11+ }
12+
13+ // Global state logger
14+ class StateLogger {
15+ private logs : StateLogEntry [ ] = [ ] ;
16+ private maxSize : number ;
17+ private isEnabled : boolean ;
18+
19+ constructor ( maxSize : number = 1000 ) {
20+ this . maxSize = maxSize ;
21+ this . isEnabled = process . env . NODE_ENV !== 'production' ;
22+ }
23+
24+ public log ( entry : StateLogEntry ) : void {
25+ if ( ! this . isEnabled ) return ;
26+
27+ this . logs . push ( entry ) ;
28+
29+ // Trim logs if they exceed max size
30+ if ( this . logs . length > this . maxSize ) {
31+ this . logs = this . logs . slice ( - this . maxSize ) ;
32+ }
33+
34+ // Also log to console for immediate visibility during development
35+ if ( process . env . NODE_ENV !== 'production' ) {
36+ console . group ( `%c${ entry . storeName } - ${ entry . action } ` , 'color: #008800; font-weight: bold;' ) ;
37+ console . log ( '%cPrevious State:' , 'color: #999;' , entry . prevState ) ;
38+ console . log ( '%cNext State:' , 'color: #008800; font-weight: bold;' , entry . nextState ) ;
39+ if ( entry . payload ) {
40+ console . log ( '%cPayload:' , 'color: #000088;' , entry . payload ) ;
41+ }
42+ console . groupEnd ( ) ;
43+ }
44+ }
45+
46+ public getLogs ( ) : StateLogEntry [ ] {
47+ return [ ...this . logs ] ;
48+ }
49+
50+ public clearLogs ( ) : void {
51+ this . logs = [ ] ;
52+ }
53+
54+ public enable ( ) : void {
55+ this . isEnabled = true ;
56+ }
57+
58+ public disable ( ) : void {
59+ this . isEnabled = false ;
60+ }
61+
62+ public getLogsByStore ( storeName : string ) : StateLogEntry [ ] {
63+ return this . logs . filter ( log => log . storeName === storeName ) ;
64+ }
65+
66+ public getLogsByAction ( action : string ) : StateLogEntry [ ] {
67+ return this . logs . filter ( log => log . action === action ) ;
68+ }
69+ }
70+
71+ // Create a singleton instance
72+ export const stateLogger = new StateLogger ( ) ;
73+
74+ // Middleware for Zustand stores to enable logging
75+ export const createDebugMiddleware = ( storeName : string ) => {
76+ return ( config : any ) => ( set : any , get : any , api : any ) => {
77+ // Wrap the original set function to intercept state changes
78+ const originalSet = set ;
79+ const enhancedSet = ( partial : any , replace ?: any ) => {
80+ const prevState = { ...get ( ) } ;
81+
82+ // Apply the state change
83+ originalSet ( partial , replace ) ;
84+
85+ const nextState = { ...get ( ) } ;
86+
87+ // Log the change
88+ stateLogger . log ( {
89+ timestamp : Date . now ( ) ,
90+ storeName,
91+ action : 'UPDATE' ,
92+ prevState,
93+ nextState,
94+ payload : typeof partial === 'function' ? 'computed update' : partial ,
95+ } ) ;
96+ } ;
97+
98+ // Return the original config with the enhanced set function
99+ return config ( enhancedSet , get , api ) ;
100+ } ;
101+ } ;
102+
103+ // State inspector utility
104+ export class StateInspector {
105+ public inspectStore ( getState : ( ) => any , storeName : string ) : any {
106+ const state = getState ( ) ;
107+ console . group ( `%cInspecting ${ storeName } State` , 'color: #0000ff; font-weight: bold;' ) ;
108+ console . table ( state ) ;
109+ console . groupEnd ( ) ;
110+ return state ;
111+ }
112+
113+ public compareStates ( prevState : any , nextState : any , storeName : string ) : void {
114+ console . group ( `%cComparing ${ storeName } States` , 'color: #ff6600; font-weight: bold;' ) ;
115+
116+ // Compare keys
117+ const prevKeys = Object . keys ( prevState ) ;
118+ const nextKeys = Object . keys ( nextState ) ;
119+
120+ const addedKeys = nextKeys . filter ( key => ! prevKeys . includes ( key ) ) ;
121+ const removedKeys = prevKeys . filter ( key => ! nextKeys . includes ( key ) ) ;
122+ const changedKeys = nextKeys . filter ( key =>
123+ prevKeys . includes ( key ) &&
124+ JSON . stringify ( prevState [ key ] ) !== JSON . stringify ( nextState [ key ] )
125+ ) ;
126+
127+ if ( addedKeys . length > 0 ) {
128+ console . log ( '%cAdded Keys:' , 'color: #00aa00;' , addedKeys ) ;
129+ }
130+
131+ if ( removedKeys . length > 0 ) {
132+ console . log ( '%cRemoved Keys:' , 'color: #aa0000;' , removedKeys ) ;
133+ }
134+
135+ if ( changedKeys . length > 0 ) {
136+ console . log ( '%cChanged Keys:' , 'color: #0000aa;' , changedKeys ) ;
137+ changedKeys . forEach ( key => {
138+ console . log ( ` ${ key } :` , prevState [ key ] , '->' , nextState [ key ] ) ;
139+ } ) ;
140+ }
141+
142+ if ( addedKeys . length === 0 && removedKeys . length === 0 && changedKeys . length === 0 ) {
143+ console . log ( '%cNo changes detected' , 'color: #888;' ) ;
144+ }
145+
146+ console . groupEnd ( ) ;
147+ }
148+
149+ public getStoreSnapshot ( getState : ( ) => any , storeName : string ) : string {
150+ const state = getState ( ) ;
151+ return JSON . stringify ( state , null , 2 ) ;
152+ }
153+ }
154+
155+ // Create a singleton inspector
156+ export const stateInspector = new StateInspector ( ) ;
157+
158+ // Performance monitoring for state updates
159+ export class StatePerformanceMonitor {
160+ private measurements : Array < {
161+ storeName : string ;
162+ action : string ;
163+ duration : number ;
164+ timestamp : number ;
165+ } > = [ ] ;
166+
167+ public measure < T > ( storeName : string , action : string , fn : ( ) => T ) : T {
168+ const start = performance . now ( ) ;
169+ const result = fn ( ) ;
170+ const end = performance . now ( ) ;
171+
172+ this . measurements . push ( {
173+ storeName,
174+ action,
175+ duration : end - start ,
176+ timestamp : Date . now ( ) ,
177+ } ) ;
178+
179+ // Keep only the last 1000 measurements
180+ if ( this . measurements . length > 1000 ) {
181+ this . measurements = this . measurements . slice ( - 1000 ) ;
182+ }
183+
184+ // Log slow updates (>16ms - one frame at 60fps)
185+ if ( end - start > 16 ) {
186+ console . warn ( `%cSlow state update detected in ${ storeName } : ${ action } took ${ ( end - start ) . toFixed ( 2 ) } ms` , 'color: #ff6600;' ) ;
187+ }
188+
189+ return result ;
190+ }
191+
192+ public getSlowUpdates ( threshold : number = 16 ) : Array < { storeName : string ; action : string ; duration : number ; timestamp : number ; } > {
193+ return this . measurements . filter ( measurement => measurement . duration > threshold ) ;
194+ }
195+
196+ public getAverageDuration ( storeName ?: string ) : number {
197+ const filteredMeasurements = storeName
198+ ? this . measurements . filter ( m => m . storeName === storeName )
199+ : this . measurements ;
200+
201+ if ( filteredMeasurements . length === 0 ) return 0 ;
202+
203+ const total = filteredMeasurements . reduce ( ( sum , m ) => sum + m . duration , 0 ) ;
204+ return total / filteredMeasurements . length ;
205+ }
206+
207+ public clearMeasurements ( ) : void {
208+ this . measurements = [ ] ;
209+ }
210+ }
211+
212+ // Create a singleton performance monitor
213+ export const statePerformanceMonitor = new StatePerformanceMonitor ( ) ;
214+
215+ // Debug utility functions
216+ export const debugUtils = {
217+ // Force trigger a re-render to test state changes
218+ forceUpdate : ( setState : ( state : any ) => void , getState : ( ) => any ) => {
219+ setState ( ( prevState : any ) => ( { ...prevState , _debugTimestamp : Date . now ( ) } ) ) ;
220+ } ,
221+
222+ // Get human-readable state summary
223+ getStateSummary : ( state : any ) : any => {
224+ const summary : any = { } ;
225+
226+ for ( const [ key , value ] of Object . entries ( state ) ) {
227+ if ( typeof value === 'function' ) {
228+ summary [ key ] = '[Function]' ;
229+ } else if ( Array . isArray ( value ) ) {
230+ summary [ key ] = `[Array: ${ value . length } items]` ;
231+ } else if ( typeof value === 'object' && value !== null ) {
232+ summary [ key ] = '[Object]' ;
233+ } else {
234+ summary [ key ] = value ;
235+ }
236+ }
237+
238+ return summary ;
239+ } ,
240+
241+ // Validate state structure
242+ validateState : ( state : any , expectedShape : any ) : boolean => {
243+ for ( const key in expectedShape ) {
244+ if ( ! ( key in state ) ) {
245+ console . error ( `Missing expected property: ${ key } ` ) ;
246+ return false ;
247+ }
248+ if ( typeof state [ key ] !== typeof expectedShape [ key ] && expectedShape [ key ] !== undefined ) {
249+ console . warn ( `Type mismatch for property: ${ key } ` ) ;
250+ }
251+ }
252+ return true ;
253+ } ,
254+
255+ // Export logs as downloadable file
256+ exportLogs : ( filename : string = 'state-logs.json' ) : void => {
257+ const logs = stateLogger . getLogs ( ) ;
258+ const blob = new Blob ( [ JSON . stringify ( logs , null , 2 ) ] , { type : 'application/json' } ) ;
259+ const url = URL . createObjectURL ( blob ) ;
260+ const a = document . createElement ( 'a' ) ;
261+ a . href = url ;
262+ a . download = filename ;
263+ document . body . appendChild ( a ) ;
264+ a . click ( ) ;
265+ document . body . removeChild ( a ) ;
266+ URL . revokeObjectURL ( url ) ;
267+ } ,
268+ } ;
0 commit comments