feat(global-state-store): implement GlobalStateStore and utils

* Create GlobalStateStore for managing application state.
* Add utility functions for loading and saving state to storage.
* Define types for global state and session management.
* Implement ProgramDataWrapper for fetching and updating program data.
This commit is contained in:
Hein
2026-02-02 13:18:33 +02:00
parent 63222f8f28
commit 29d56980b2
5 changed files with 564 additions and 0 deletions

View File

@@ -0,0 +1,237 @@
import type { StoreApi } from 'zustand';
import { produce } from 'immer';
import { shallow } from 'zustand/shallow';
import { useStoreWithEqualityFn } from 'zustand/traditional';
import { createStore } from 'zustand/vanilla';
import type { ExtractState, GlobalState, GlobalStateStoreState } from './GlobalStateStore.types';
import { loadStorage, saveStorage } from './GlobalStateStore.utils';
const emptyStore: GlobalState = {
connected: true, //Only invalidate when a connection cannot be made
controls: {},
environment: 'production',
loading: false,
meta: {},
program: {
name: '',
slug: '',
},
user: {
access_control: false,
avatar_docid: 0,
id: 0,
login: '',
name: 'Guest',
},
};
//We use vanilla store because we must be able to get the API key and token outside a react render loop
//The storage is custom because zustand's vanilla stores persist API crashes.
//Also not using the other store because it's using outdated methods and give that warning
/**
* A zustand store function for managing program data and session information.
*
* @returns A zustand store state object.
*/
const GlobalStateStore = createStore<GlobalStateStoreState>((set, get) => ({
...emptyStore,
fetchData: async (url?: string) => {
const setFetched = async (
fn: (partial: GlobalState | Partial<GlobalState>) => Partial<GlobalStateStoreState>
) => {
const state = fn(get());
set((cur) => {
return { ...cur, ...state };
});
};
try {
set((s) => ({
...s,
loading: true,
session: { ...s.session, apiURL: url ?? s.session.apiURL },
}));
const result = get().onFetchSession?.(get());
await setFetched((s) => ({
...s,
...result,
connected: true,
loading: false,
updatedAt: new Date().toISOString(),
}));
} catch (e) {
await setFetched((s) => ({
...s,
connected: false,
error: `Load Exception: ${String(e)}`,
loading: false,
}));
}
},
login: async (sessionData?: string) => {
const state = get();
const newstate = {
...state,
session: { ...state.session, authtoken: sessionData ?? '' },
user: { ...state.user },
};
set((cur) => {
return { ...cur, ...newstate };
});
await get().fetchData();
},
logout: async () => {
const newstate = { ...get(), ...emptyStore };
set((state) => {
return { ...state, ...newstate };
});
await get().fetchData();
},
setAuthToken: (token: string) =>
set(
produce((state) => {
state.session.authtoken = token;
})
),
setIsSecurity: (issecurity: boolean) =>
set(
produce((state) => {
state.session.jsonvalue.issecurity = issecurity;
})
),
setState: (key, value) =>
set(
produce((state) => {
state[key] = value;
})
),
setStateFN: (key, value) => {
set(
produce((state) => {
if (typeof value === 'function') {
state[key] = (value as (value: any) => any)(state[key]);
} else {
console.error('value is not a function', value);
}
})
);
},
updateSession: (setter: UpdateSessionType) => {
const curState = get();
const newSession: null | SessionDetail | void =
typeof setter === 'function'
? setter(curState?.session)
: typeof setter === 'object'
? (setter as SessionDetail)
: null;
if (newSession === null) {
return;
}
const updatedState = {
...curState,
session: { ...curState.session, ...(newSession || {}) },
};
set((state) => {
state = {
...state,
session: { ...state.session, ...updatedState.session },
};
return state;
});
},
}));
//Load storage after the createStore function is executed.
try {
loadStorage()
.then((state) =>
GlobalStateStore.setState((s: GlobalStateStoreState) => ({
...s,
...state,
}))
)
.catch((e) => {
console.error('Error loading storage:', e);
});
GlobalStateStore.subscribe((state, previousState) => {
//console.log('subscribe', state, previousState)
saveStorage(state).catch((e) => {
console.error('Error saving storage:', e);
});
if (state.session.authtoken !== previousState.session.authtoken) {
setAuthTokenAPI(state.session.authtoken);
}
});
} catch (e) {
console.error('Error loading storage:', e);
}
/**
* Type-bounded version of useStore with shallow equality build in
*/
const createTypeBoundedUseStore = ((store) => (selector) =>
useStoreWithEqualityFn(store, selector, shallow)) as <S extends StoreApi<unknown>>(
store: S
) => {
(): ExtractState<S>;
<T>(selector: (state: ExtractState<S>) => T): T;
};
/**
* Creates a hook to access the state of the `GlobalStateStore` with shallow equality
* checking in the selector function.
*
* @typeParam S - The type of the store
* @param store - The store to be used
* @returns A function that returns the state of the store, or a selected part of it
*/
const useGlobalStateStore = createTypeBoundedUseStore(GlobalStateStore);
/**
* Sets the API URL in the program data store state.
*
* @param {string} url - The URL to set as the API URL.
* @return {void}
*/
const setApiURL = (url: string) => {
if (typeof GlobalStateStore?.setState !== 'function') {
return;
}
GlobalStateStore.setState((s: GlobalStateStoreState) => ({
...s,
session: {
...s.session,
apiURL: url,
},
}));
};
/**
* Retrieves the API URL from the session stored in the program data store.
*
* @return {string} The API URL from the session.
*/
const getApiURL = (): string => {
if (typeof GlobalStateStore?.setState !== 'function') {
return '';
}
const s = GlobalStateStore.getState();
return s.session?.apiURL;
};
export { getApiURL, GlobalStateStore, setApiURL, useGlobalStateStore };