import type { StoreApi } from 'zustand'; import { shallow } from 'zustand/shallow'; import { useStoreWithEqualityFn } from 'zustand/traditional'; import { createStore } from 'zustand/vanilla'; import type { BarState, ExtractState, GlobalState, GlobalStateStoreType, LayoutState, NavigationState, OwnerState, ProgramState, SessionState, UserState, } from './GlobalStateStore.types'; import { loadStorage, saveStorage } from './GlobalStateStore.utils'; const initialState: GlobalState = { initialized: false, layout: { bottomBar: { open: false }, leftBar: { open: false }, rightBar: { open: false }, topBar: { open: false }, }, navigation: { menu: [], }, owner: { guid: '', id: 0, name: '', }, program: { controls: {}, environment: 'production', guid: '', name: '', slug: '', }, session: { apiURL: '', authToken: '', connected: true, loading: false, loggedIn: false, }, user: { guid: '', username: '', }, }; type GetState = () => GlobalStateStoreType; type SetState = ( partial: ((state: GlobalState) => Partial) | Partial ) => void; const createProgramSlice = (set: SetState) => ({ setProgram: (updates: Partial) => set((state: GlobalState) => ({ program: { ...state.program, ...updates }, })), }); const createSessionSlice = (set: SetState) => ({ setApiURL: (url: string) => set((state: GlobalState) => ({ session: { ...state.session, apiURL: url }, })), setAuthToken: (token: string) => set((state: GlobalState) => ({ session: { ...state.session, authToken: token }, })), setSession: (updates: Partial) => set((state: GlobalState) => ({ session: { ...state.session, ...updates }, })), }); const createOwnerSlice = (set: SetState) => ({ setOwner: (updates: Partial) => set((state: GlobalState) => ({ owner: { ...state.owner, ...updates }, })), }); const createUserSlice = (set: SetState) => ({ setUser: (updates: Partial) => set((state: GlobalState) => ({ user: { ...state.user, ...updates }, })), }); const createLayoutSlice = (set: SetState) => ({ setBottomBar: (updates: Partial) => set((state: GlobalState) => ({ layout: { ...state.layout, bottomBar: { ...state.layout.bottomBar, ...updates } }, })), setLayout: (updates: Partial) => set((state: GlobalState) => ({ layout: { ...state.layout, ...updates }, })), setLeftBar: (updates: Partial) => set((state: GlobalState) => ({ layout: { ...state.layout, leftBar: { ...state.layout.leftBar, ...updates } }, })), setRightBar: (updates: Partial) => set((state: GlobalState) => ({ layout: { ...state.layout, rightBar: { ...state.layout.rightBar, ...updates } }, })), setTopBar: (updates: Partial) => set((state: GlobalState) => ({ layout: { ...state.layout, topBar: { ...state.layout.topBar, ...updates } }, })), }); const createNavigationSlice = (set: SetState) => ({ setCurrentPage: (page: NavigationState['currentPage']) => set((state: GlobalState) => ({ navigation: { ...state.navigation, currentPage: page }, })), setMenu: (menu: NavigationState['menu']) => set((state: GlobalState) => ({ navigation: { ...state.navigation, menu }, })), setNavigation: (updates: Partial) => set((state: GlobalState) => ({ navigation: { ...state.navigation, ...updates }, })), }); const createComplexActions = (set: SetState, get: GetState) => { // Internal implementation without lock const fetchDataInternal = async (url?: string) => { try { set((state: GlobalState) => ({ session: { ...state.session, apiURL: url || state.session.apiURL, loading: true, }, })); const currentState = get(); const result = await currentState.onFetchSession?.(currentState); set((state: GlobalState) => ({ ...state, ...result, layout: { ...state.layout, ...result?.layout }, navigation: { ...state.navigation, ...result?.navigation }, owner: { ...state.owner, ...result?.owner, }, program: { ...state.program, ...result?.program, updatedAt: new Date().toISOString(), }, session: { ...state.session, ...result?.session, connected: true, loading: false, }, user: { ...state.user, ...result?.user, }, })); } catch (e) { set((state: GlobalState) => ({ session: { ...state.session, connected: false, error: `Load Exception: ${String(e)}`, loading: false, }, })); } }; return { fetchData: async (url?: string) => { // Wait for initialization to complete await waitForInitialization(); // Use lock to prevent concurrent fetchData calls return withOperationLock(() => fetchDataInternal(url)); }, isLoggedIn: (): boolean => { const session = get().session; if (!session.loggedIn || !session.authToken) { return false; } if (session.expiryDate && new Date(session.expiryDate) < new Date()) { return false; } return true; }, login: async (authToken?: string, user?: Partial) => { // Wait for initialization to complete await waitForInitialization(); // Use lock to prevent concurrent auth operations return withOperationLock(async () => { try { set((state: GlobalState) => ({ session: { ...state.session, authToken: authToken ?? '', expiryDate: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), loading: true, loggedIn: true, }, user: { ...state.user, ...user, }, })); const currentState = get(); const result = await currentState.onLogin?.(currentState); if (result) { set((state: GlobalState) => ({ ...state, owner: result.owner ? { ...state.owner, ...result.owner } : state.owner, program: result.program ? { ...state.program, ...result.program } : state.program, session: result.session ? { ...state.session, ...result.session } : state.session, user: result.user ? { ...state.user, ...result.user } : state.user, })); } // Call internal version to avoid nested lock await fetchDataInternal(); } catch (e) { set((state: GlobalState) => ({ session: { ...state.session, connected: false, error: `Login Exception: ${String(e)}`, loading: false, loggedIn: false, }, })); } finally { set((state: GlobalState) => ({ session: { ...state.session, loading: false, }, })); } }); }, logout: async () => { // Wait for initialization to complete await waitForInitialization(); // Use lock to prevent concurrent auth operations return withOperationLock(async () => { try { set((state: GlobalState) => ({ ...initialState, session: { ...initialState.session, apiURL: state.session.apiURL, expiryDate: undefined, loading: true, loggedIn: false, }, })); const currentState = get(); const result = await currentState.onLogout?.(currentState); if (result) { set((state: GlobalState) => ({ ...state, owner: result.owner ? { ...state.owner, ...result.owner } : state.owner, program: result.program ? { ...state.program, ...result.program } : state.program, session: result.session ? { ...state.session, ...result.session } : state.session, user: result.user ? { ...state.user, ...result.user } : state.user, })); } // Call internal version to avoid nested lock await fetchDataInternal(); } catch (e) { set((state: GlobalState) => ({ session: { ...state.session, connected: false, error: `Logout Exception: ${String(e)}`, loading: false, }, })); } finally { set((state: GlobalState) => ({ session: { ...state.session, loading: false, }, })); } }); }, }; }; // State management flags and locks - must be defined before store creation let isStorageInitialized = false; let initializationPromise: null | Promise = null; let operationLock: Promise = Promise.resolve(); // Helper to wait for initialization - must be defined before store creation const waitForInitialization = async (): Promise => { if (initializationPromise) { await initializationPromise; } }; // Helper to ensure async operations run sequentially const withOperationLock = async (operation: () => Promise): Promise => { const currentLock = operationLock; let releaseLock: () => void; // Create new lock promise operationLock = new Promise((resolve) => { releaseLock = resolve; }); try { // Wait for previous operation to complete await currentLock; // Run the operation return await operation(); } finally { // Release the lock releaseLock!(); } }; const GlobalStateStore = createStore((set, get) => ({ ...initialState, ...createProgramSlice(set), ...createSessionSlice(set), ...createOwnerSlice(set), ...createUserSlice(set), ...createLayoutSlice(set), ...createNavigationSlice(set), ...createComplexActions(set, get), })); // Initialize storage and load saved state initializationPromise = loadStorage() .then((state) => { // Merge loaded state with initial state GlobalStateStore.setState( (current) => ({ ...current, ...state, initialized: true, session: { ...current.session, ...state.session, connected: true, loading: false, }, }), true // Replace state completely to avoid triggering subscription during init ); }) .catch((e) => { console.error('Error loading storage:', e); // Mark as initialized even on error so app doesn't hang GlobalStateStore.setState({ initialized: true }); }) .finally(() => { // Mark initialization as complete isStorageInitialized = true; initializationPromise = null; }); // Subscribe to state changes and persist to storage // Only saves after initialization is complete GlobalStateStore.subscribe((state) => { if (!isStorageInitialized) { return; } saveStorage(state).catch((e) => { console.error('Error saving storage:', e); }); }); const createTypeBoundedUseStore = ((store) => (selector) => useStoreWithEqualityFn(store, selector, shallow)) as >( store: S ) => { (): ExtractState; (selector: (state: ExtractState) => T): T; }; const useGlobalStateStore = createTypeBoundedUseStore(GlobalStateStore); const setApiURL = (url: string) => { GlobalStateStore.getState().setApiURL(url); }; const getApiURL = (): string => { return GlobalStateStore.getState().session.apiURL ?? ''; }; const getAuthToken = (): string => { return GlobalStateStore.getState().session.authToken ?? ''; }; const isLoggedIn = (): boolean => { return GlobalStateStore.getState().isLoggedIn(); }; const setAuthToken = (token: string) => { GlobalStateStore.getState().setAuthToken(token); }; const GetGlobalState = (): GlobalStateStoreType => { return GlobalStateStore.getState(); }; export { getApiURL, getAuthToken, GetGlobalState, GlobalStateStore, isLoggedIn, setApiURL, setAuthToken, useGlobalStateStore, };