Skip to content

Commit 167a42d

Browse files
Merge pull request #66 from Richiey1/feat/state-management
feat: #52 Implement Advanced State Management
2 parents 3263e0f + f32d2b5 commit 167a42d

File tree

8 files changed

+401
-0
lines changed

8 files changed

+401
-0
lines changed

src/app/layout.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ import { InternationalizationEngine } from "@/components/i18n/Internationalizati
88
import { CulturalAdaptationManager } from "@/components/i18n/CulturalAdaptationManager";
99
import PerformanceMonitor from "@/components/performance/PerformanceMonitor";
1010
import PrefetchingEngine from "@/components/performance/PrefetchingEngine";
11+
import StateManagerIntegration from "@/components/state/StateManagerIntegration";
1112

1213
const geistSans = Geist({
14+
// ...
1315
variable: "--font-geist-sans",
1416
subsets: ["latin"],
1517
});
@@ -39,6 +41,7 @@ export default function RootLayout({
3941
<CulturalAdaptationManager>
4042
<ThemeProvider>
4143
<OfflineModeProvider>
44+
<StateManagerIntegration />
4245
<PerformanceMonitor />
4346
<PrefetchingEngine />
4447
{children}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
'use client';
2+
3+
import { useEffect } from 'react';
4+
import { syncEngine } from '../../store/synchronizationEngine';
5+
import { inspectState } from '../../store/devTools';
6+
import { useStore } from '../../store/stateManager';
7+
8+
/**
9+
* Component to handle side-effects for state management (Sync, DevTools).
10+
* This should be rendered once in the root layout.
11+
*/
12+
export const StateManagerIntegration = () => {
13+
useEffect(() => {
14+
// 1. Initialize Sync Engine
15+
// (Constructor already does it, but we ensure it's loaded in the browser)
16+
console.log('[StateManager] Integration active');
17+
18+
// 2. Attach DevTools
19+
inspectState(useStore);
20+
21+
return () => {
22+
// 3. Cleanup Sync Engine
23+
syncEngine.disconnect();
24+
};
25+
}, []);
26+
27+
return null; // This is a headless component
28+
};
29+
30+
export default StateManagerIntegration;

src/hooks/useOptimisticUpdates.tsx

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { useState, useCallback } from 'react';
2+
3+
/**
4+
* Hook for handling optimistic UI updates with rollback support.
5+
*/
6+
export const useOptimisticUpdates = <T,>() => {
7+
const [isUpdating, setIsUpdating] = useState(false);
8+
const [error, setError] = useState<Error | null>(null);
9+
10+
/**
11+
* Executes an optimistic update.
12+
* @param updateFn Function to perform the local state update.
13+
* @param apiFn Function to perform the actual API call.
14+
* @param rollbackFn Function to reverse the local state update on failure.
15+
*/
16+
const executeUpdate = useCallback(async (
17+
updateFn: () => void,
18+
apiFn: () => Promise<T>,
19+
rollbackFn: () => void
20+
) => {
21+
setIsUpdating(true);
22+
setError(null);
23+
24+
// 1. Perform optimistic update
25+
try {
26+
updateFn();
27+
} catch (e) {
28+
console.error('[OptimisticUpdate] Local update failed:', e);
29+
setIsUpdating(false);
30+
return;
31+
}
32+
33+
// 2. Execute API call
34+
try {
35+
const result = await apiFn();
36+
setIsUpdating(false);
37+
return result;
38+
} catch (e) {
39+
// 3. Rollback on failure
40+
console.error('[OptimisticUpdate] API call failed, rolling back:', e);
41+
setError(e instanceof Error ? e : new Error(String(e)));
42+
rollbackFn();
43+
setIsUpdating(false);
44+
throw e;
45+
}
46+
}, []);
47+
48+
return { executeUpdate, isUpdating, error };
49+
};

src/store/devTools.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/**
2+
* Development tools and debugging utilities for state management.
3+
*/
4+
5+
/**
6+
* Middleware or utility to log state transitions in development.
7+
*/
8+
import { StateCreator } from 'zustand';
9+
10+
/**
11+
* Middleware or utility to log state transitions in development.
12+
*/
13+
export const stateLogger = <T extends object>(
14+
config: StateCreator<T, any, any>
15+
): StateCreator<T, any, any> => (set, get, api) =>
16+
config(
17+
(args) => {
18+
if (process.env.NODE_ENV === 'development') {
19+
console.group('%c [State Action]', 'color: #00bcd4; font-weight: bold;');
20+
console.log('Action:', args);
21+
console.log('Previous State:', get());
22+
set(args);
23+
console.log('Next State:', get());
24+
console.groupEnd();
25+
} else {
26+
set(args);
27+
}
28+
},
29+
get,
30+
api
31+
);
32+
33+
/**
34+
* Utility to inspect the current state in the console.
35+
*/
36+
export const inspectState = (store: any) => {
37+
if (typeof window !== 'undefined') {
38+
(window as any).__TEACHLINK_STATE__ = store.getState;
39+
console.log('[DevTools] State inspector attached to window.__TEACHLINK_STATE__');
40+
}
41+
};

src/store/persistenceLayer.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { openDB } from 'idb';
2+
3+
const DB_NAME = 'teachlink_state_v1';
4+
const STORE_NAME = 'app_state';
5+
6+
/**
7+
* Persistence layer using IndexedDB for large state objects.
8+
*/
9+
export const persistenceLayer = {
10+
/**
11+
* Loads the state from IndexedDB.
12+
*/
13+
async getItem(name: string): Promise<string | null> {
14+
try {
15+
const db = await openDB(DB_NAME, 1, {
16+
upgrade(db) {
17+
db.createObjectStore(STORE_NAME);
18+
},
19+
});
20+
const data = await db.get(STORE_NAME, name);
21+
return data ? JSON.stringify(data) : null;
22+
} catch (error) {
23+
console.error('[Persistence] Error loading state:', error);
24+
return null;
25+
}
26+
},
27+
28+
/**
29+
* Saves the state to IndexedDB.
30+
*/
31+
async setItem(name: string, value: string): Promise<void> {
32+
try {
33+
const db = await openDB(DB_NAME, 1, {
34+
upgrade(db) {
35+
db.createObjectStore(STORE_NAME);
36+
},
37+
});
38+
await db.put(STORE_NAME, JSON.parse(value), name);
39+
} catch (error) {
40+
console.error('[Persistence] Error saving state:', error);
41+
}
42+
},
43+
44+
/**
45+
* Removes the state from IndexedDB.
46+
*/
47+
async removeItem(name: string): Promise<void> {
48+
const db = await openDB(DB_NAME, 1);
49+
await db.delete(STORE_NAME, name);
50+
},
51+
};

src/store/stateManager.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { create } from 'zustand';
2+
import { persist, createJSONStorage } from 'zustand/middleware';
3+
import { persistenceLayer } from './persistenceLayer';
4+
import { deepMerge } from '../utils/stateUtils';
5+
import { stateLogger } from './devTools';
6+
7+
interface UserState {
8+
id: string | null;
9+
name: string | null;
10+
preferences: {
11+
theme: 'light' | 'dark';
12+
language: string;
13+
notifications: boolean;
14+
};
15+
}
16+
17+
interface AppState {
18+
isSidebarOpen: boolean;
19+
offlineMode: boolean;
20+
lastSynced: number | null;
21+
}
22+
23+
interface StoreState {
24+
user: UserState;
25+
app: AppState;
26+
27+
// Actions
28+
setUser: (user: Partial<UserState>) => void;
29+
setPreferences: (prefs: Partial<UserState['preferences']>) => void;
30+
toggleSidebar: () => void;
31+
setOfflineMode: (mode: boolean) => void;
32+
updateSyncTime: () => void;
33+
34+
// Entire state replacement (used by sync engine)
35+
rehydrate: (state: Partial<StoreState>) => void;
36+
}
37+
38+
/**
39+
* Centralized state manager using Zustand with persistence.
40+
*/
41+
export const useStore = create<StoreState>()(
42+
stateLogger(
43+
persist(
44+
(set) => ({
45+
user: {
46+
id: null,
47+
name: null,
48+
preferences: {
49+
theme: 'light' as 'light' | 'dark',
50+
language: 'en',
51+
notifications: true,
52+
},
53+
},
54+
app: {
55+
isSidebarOpen: true,
56+
offlineMode: false,
57+
lastSynced: null,
58+
},
59+
60+
setUser: (user: Partial<UserState>) =>
61+
set((state: StoreState) => ({ user: { ...state.user, ...user } })),
62+
63+
setPreferences: (prefs: Partial<UserState['preferences']>) =>
64+
set((state: StoreState) => ({
65+
user: {
66+
...state.user,
67+
preferences: { ...state.user.preferences, ...prefs },
68+
},
69+
})),
70+
71+
toggleSidebar: () =>
72+
set((state: StoreState) => ({ app: { ...state.app, isSidebarOpen: !state.app.isSidebarOpen } })),
73+
74+
setOfflineMode: (mode: boolean) =>
75+
set((state: StoreState) => ({ app: { ...state.app, offlineMode: mode } })),
76+
77+
updateSyncTime: () =>
78+
set((state: StoreState) => ({ app: { ...state.app, lastSynced: Date.now() } })),
79+
80+
rehydrate: (newState: Partial<StoreState>) =>
81+
set((state: StoreState) => deepMerge(state as unknown as Record<string, unknown>, newState as Record<string, unknown>) as unknown as StoreState),
82+
}),
83+
{
84+
name: 'teachlink-storage',
85+
storage: createJSONStorage(() => persistenceLayer),
86+
partialize: (state: StoreState) => ({
87+
user: state.user,
88+
app: state.app,
89+
}), // Only persist these fields
90+
}
91+
)
92+
)
93+
);

src/store/synchronizationEngine.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { useStore } from './stateManager';
2+
3+
const CHANNEL_NAME = 'teachlink_state_sync';
4+
5+
/**
6+
* Synchronization engine for keeping state in sync across multiple browser tabs.
7+
*/
8+
export class SynchronizationEngine {
9+
private channel: BroadcastChannel | null = null;
10+
private isProcessingSync = false;
11+
12+
constructor() {
13+
if (typeof window !== 'undefined' && 'BroadcastChannel' in window) {
14+
this.channel = new BroadcastChannel(CHANNEL_NAME);
15+
this.setupListeners();
16+
}
17+
}
18+
19+
/**
20+
* Sets up the broadcast channel listeners.
21+
*/
22+
private setupListeners() {
23+
if (!this.channel) return;
24+
25+
this.channel.onmessage = (event) => {
26+
if (this.isProcessingSync) return;
27+
28+
const { type, payload } = event.data;
29+
30+
if (type === 'STATE_UPDATE') {
31+
console.log('[SyncEngine] Received state update from another tab');
32+
this.isProcessingSync = true;
33+
34+
// Update the local store with the external state
35+
useStore.getState().rehydrate(payload);
36+
37+
this.isProcessingSync = false;
38+
}
39+
};
40+
41+
// Subscribing to store changes to broadcast them
42+
useStore.subscribe((state, prevState) => {
43+
if (this.isProcessingSync) return;
44+
45+
// Simple check to avoid unnecessary broadcasts
46+
// In a real scenario, you might want a deeper comparison or specific action tracking
47+
if (JSON.stringify(state) !== JSON.stringify(prevState)) {
48+
this.broadcastState(state);
49+
}
50+
});
51+
}
52+
53+
/**
54+
* Broadcasts the current state to other tabs.
55+
*/
56+
private broadcastState(state: any) {
57+
if (!this.channel) return;
58+
59+
console.log('[SyncEngine] Broadcasting state update to other tabs');
60+
this.channel.postMessage({
61+
type: 'STATE_UPDATE',
62+
payload: state,
63+
});
64+
}
65+
66+
/**
67+
* Disconnects the sync engine.
68+
*/
69+
public disconnect() {
70+
if (this.channel) {
71+
this.channel.close();
72+
this.channel = null;
73+
}
74+
}
75+
}
76+
77+
// Global instance
78+
export const syncEngine = new SynchronizationEngine();

0 commit comments

Comments
 (0)