Skip to content

Commit 4a5bae7

Browse files
authored
Merge pull request #38 from feyishola/feat/state-mgt-arch
state management feature implemented
2 parents 28de34e + d45a07c commit 4a5bae7

7 files changed

Lines changed: 520 additions & 41 deletions

File tree

src/hooks/usePropertySearch.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export function usePropertySearch() {
3232
setProperties,
3333
setLoading,
3434
setError,
35+
lastUpdated,
3536
} = useSearchStore();
3637

3738
// Initialize from URL parameters on mount
@@ -122,6 +123,7 @@ export function usePropertySearch() {
122123
totalPages,
123124
isLoading,
124125
error,
126+
lastUpdated,
125127

126128
// Actions
127129
setFilter: handleFilterChange,

src/store/base.ts

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { create } from 'zustand';
2+
import { persist, PersistOptions } from 'zustand/middleware';
3+
4+
// Base state interface for common properties
5+
export interface BaseState {
6+
isLoading: boolean;
7+
error: string | null;
8+
lastUpdated: number | null;
9+
}
10+
11+
// Base actions interface for common operations
12+
export interface BaseActions<T = {}> {
13+
setLoading: (loading: boolean) => void;
14+
setError: (error: string | null) => void;
15+
clearError: () => void;
16+
setLastUpdated: (timestamp: number) => void;
17+
reset: () => void;
18+
}
19+
20+
// Base store type combining state and actions
21+
export type BaseStore<T extends BaseState> = T & BaseActions;
22+
23+
// Store configuration options
24+
export interface StoreConfig {
25+
persist?: boolean;
26+
persistOptions?: PersistOptions<any, any>;
27+
name: string;
28+
}
29+
30+
// Enhanced create function with base functionality
31+
export const createBaseStore = <
32+
T extends BaseState,
33+
A extends BaseActions
34+
>(
35+
initialState: T,
36+
actions: (set: any, get: any) => A,
37+
config?: StoreConfig
38+
) => {
39+
const store = (set: any, get: any) => ({
40+
...initialState,
41+
...actions(set, get),
42+
});
43+
44+
if (config?.persist) {
45+
return create<BaseStore<T> & A>()(
46+
persist(store, {
47+
name: config.name,
48+
...config.persistOptions,
49+
})
50+
);
51+
}
52+
53+
return create<BaseStore<T> & A>()(store);
54+
};
55+
56+
// Selector helpers for performance optimization
57+
export const createSelector = <T, R>(
58+
selector: (state: T) => R
59+
): ((state: T) => R) => selector;
60+
61+
// Async action wrapper for consistent error handling
62+
export const withAsyncAction = async <T>(
63+
action: () => Promise<T>,
64+
setError: (error: string | null) => void,
65+
setLoading: (loading: boolean) => void
66+
): Promise<T> => {
67+
try {
68+
setLoading(true);
69+
setError(null);
70+
const result = await action();
71+
return result;
72+
} catch (error: any) {
73+
const errorMessage = error?.message || 'An unknown error occurred';
74+
setError(errorMessage);
75+
throw error;
76+
} finally {
77+
setLoading(false);
78+
}
79+
};
80+
81+
// State persistence utilities
82+
export const clearAllPersistedState = () => {
83+
Object.keys(localStorage).forEach(key => {
84+
if (key.startsWith('propchain-')) {
85+
localStorage.removeItem(key);
86+
}
87+
});
88+
};
89+
90+
// Memoization helper for derived state
91+
export const createMemoizedSelector = <T, R>(
92+
selector: (state: T) => R,
93+
equalityFn: (a: R, b: R) => boolean = Object.is
94+
) => {
95+
let lastResult: R | undefined;
96+
let lastInput: T | undefined;
97+
98+
return (state: T): R => {
99+
if (lastInput === undefined || !equalityFn(selector(lastInput), selector(state))) {
100+
lastResult = selector(state);
101+
lastInput = state;
102+
}
103+
return lastResult!;
104+
};
105+
};

src/store/debug.ts

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
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

Comments
 (0)