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:
237
src/GlobalStateStore/GlobalStateStore.ts
Normal file
237
src/GlobalStateStore/GlobalStateStore.ts
Normal 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 };
|
||||
Reference in New Issue
Block a user