Compare commits
2 Commits
main
...
dev-global
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a81d59f3ba | ||
|
|
29d56980b2 |
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 };
|
||||
92
src/GlobalStateStore/GlobalStateStore.types.ts
Normal file
92
src/GlobalStateStore/GlobalStateStore.types.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { type FunctionComponent } from 'react';
|
||||
|
||||
type DatabaseDetail = {
|
||||
name?: string;
|
||||
version?: string;
|
||||
};
|
||||
|
||||
type ExtractState<S> = S extends { getState: () => infer X } ? X : never;
|
||||
|
||||
interface GlobalState {
|
||||
[key: string]: any;
|
||||
apiURL: string;
|
||||
authtoken: string;
|
||||
connected?: boolean;
|
||||
environment?: 'development' | 'production';
|
||||
error?: string;
|
||||
globals?: Record<string, any>;
|
||||
lastLoadTime?: string;
|
||||
loading?: boolean;
|
||||
menu?: Array<any>;
|
||||
meta?: ProgramMetaData;
|
||||
program: ProgramDetail;
|
||||
|
||||
updatedAt?: string;
|
||||
user: UserDetail;
|
||||
}
|
||||
|
||||
interface GlobalStateStoreState extends GlobalState {
|
||||
fetchData: (url?: string) => Promise<void>;
|
||||
login: (sessionData?: string) => Promise<void>;
|
||||
logout: () => Promise<void>;
|
||||
onFetchSession?: (state: GlobalState) => Promise<GlobalState>;
|
||||
setAuthToken: (token: string) => void;
|
||||
setIsSecurity: (isSecurity: boolean) => void;
|
||||
setState: <K extends keyof GlobalState>(key: K, value: GlobalState[K]) => void;
|
||||
setStateFN: <K extends keyof GlobalState>(
|
||||
key: K,
|
||||
value: (current: GlobalState[K]) => Partial<GlobalState[K]>
|
||||
) => void;
|
||||
}
|
||||
|
||||
type ProgramDetail = {
|
||||
backend_version?: string;
|
||||
biglogolink?: string;
|
||||
database?: DatabaseDetail;
|
||||
database_version?: string;
|
||||
logolink?: string;
|
||||
name: string;
|
||||
programSummary?: string;
|
||||
rid_owner?: number;
|
||||
slug: string;
|
||||
version?: string;
|
||||
};
|
||||
|
||||
interface ProgramMetaData {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface ProgramWrapperProps {
|
||||
apiURL?: string;
|
||||
children: React.ReactNode | React.ReactNode[];
|
||||
debugMode?: boolean;
|
||||
fallback?: React.ReactNode | React.ReactNode[];
|
||||
renderFallback?: boolean;
|
||||
testMode?: boolean;
|
||||
version?: string;
|
||||
}
|
||||
|
||||
type UserDetail = {
|
||||
access_control?: boolean;
|
||||
avatar_docid?: number;
|
||||
fullnames?: string;
|
||||
guid?: string;
|
||||
id?: number;
|
||||
isadmin?: boolean;
|
||||
login?: string;
|
||||
name?: string;
|
||||
notice_msg?: string;
|
||||
parameters?: Record<string, any>;
|
||||
rid_hub?: number;
|
||||
rid_user?: number;
|
||||
secuser?: Record<string, any>;
|
||||
};
|
||||
|
||||
export type {
|
||||
ExtractState,
|
||||
GlobalState,
|
||||
GlobalStateStoreState,
|
||||
ProgramDetail,
|
||||
ProgramWrapperProps,
|
||||
UserDetail,
|
||||
};
|
||||
109
src/GlobalStateStore/GlobalStateStore.utils.ts
Normal file
109
src/GlobalStateStore/GlobalStateStore.utils.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { createStore, entries, set, type UseStore } from 'idb-keyval';
|
||||
|
||||
const STORAGE_KEY = 'app-data';
|
||||
|
||||
const initilizeStore = () => {
|
||||
if (indexedDB) {
|
||||
try {
|
||||
return createStore('programdata', 'programdata');
|
||||
} catch (e) {
|
||||
console.error('Failed to initialize indexedDB store: ', STORAGE_KEY, e);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const programDataIndexDBStore: null | UseStore = initilizeStore();
|
||||
|
||||
const skipKeysCallback = (dataKey: string, dataValue: any) => {
|
||||
if (typeof dataValue === 'function') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (
|
||||
dataKey === 'loading' ||
|
||||
dataKey === 'error' ||
|
||||
dataKey === 'security' ||
|
||||
dataKey === 'meta' ||
|
||||
dataKey === 'help'
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return dataValue;
|
||||
};
|
||||
|
||||
async function loadStorage<T = any>(storageKey?: string): Promise<T> {
|
||||
if (indexedDB) {
|
||||
try {
|
||||
const storeValues = await entries(programDataIndexDBStore);
|
||||
const obj: any = {};
|
||||
|
||||
storeValues.forEach((arr: string[]) => {
|
||||
const k = String(arr[0]);
|
||||
obj[k] = JSON.parse(arr[1]);
|
||||
});
|
||||
|
||||
return obj;
|
||||
} catch (e) {
|
||||
console.error('Failed to load storage: ', storageKey ?? STORAGE_KEY, e);
|
||||
}
|
||||
} else if (localStorage) {
|
||||
try {
|
||||
const storagedata = localStorage.getItem(storageKey ?? STORAGE_KEY);
|
||||
if (storagedata && storagedata.length > 0) {
|
||||
const obj = JSON.parse(storagedata, (_dataKey, dataValue) => {
|
||||
if (typeof dataValue === 'string' && dataValue.startsWith('function')) {
|
||||
return undefined;
|
||||
}
|
||||
return dataValue;
|
||||
});
|
||||
return obj;
|
||||
}
|
||||
return {} as T;
|
||||
} catch (e) {
|
||||
console.error('Failed to load storage: ', storageKey ?? STORAGE_KEY, e);
|
||||
}
|
||||
}
|
||||
|
||||
return {} as T;
|
||||
}
|
||||
|
||||
async function saveStorage<T = any>(data: T, storageKey?: string): Promise<T> {
|
||||
if (indexedDB) {
|
||||
try {
|
||||
const keys = Object.keys(data as object).filter(
|
||||
(key) =>
|
||||
key !== 'loading' &&
|
||||
key !== 'error' &&
|
||||
key !== 'help' &&
|
||||
key !== 'meta' &&
|
||||
key !== 'security' &&
|
||||
typeof data[key as keyof T] !== 'function'
|
||||
);
|
||||
const promises = keys.map((key) => {
|
||||
return set(
|
||||
key,
|
||||
JSON.stringify((data as any)[key], skipKeysCallback) ?? '{}',
|
||||
programDataIndexDBStore
|
||||
);
|
||||
});
|
||||
await Promise.all(promises);
|
||||
return data;
|
||||
} catch (e) {
|
||||
console.error('Failed to save indexedDB storage: ', storageKey ?? STORAGE_KEY, e);
|
||||
}
|
||||
} else if (localStorage) {
|
||||
try {
|
||||
const dataString = JSON.stringify(data, skipKeysCallback);
|
||||
|
||||
localStorage.setItem(storageKey ?? STORAGE_KEY, dataString ?? '{}');
|
||||
return data;
|
||||
} catch (e) {
|
||||
console.error('Failed to save localStorage storage: ', storageKey ?? STORAGE_KEY, e);
|
||||
}
|
||||
}
|
||||
return {} as T;
|
||||
}
|
||||
|
||||
export { loadStorage, saveStorage };
|
||||
0
src/GlobalStateStore/GlobalStateStoreWrapper.tsx
Normal file
0
src/GlobalStateStore/GlobalStateStoreWrapper.tsx
Normal file
9
src/GlobalStateStore/index.ts
Normal file
9
src/GlobalStateStore/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export { ProgramDataWrapper } from './src/ProgramDataWrapper'
|
||||
export {
|
||||
getApiURL,
|
||||
setApiURL,
|
||||
programDataStore,
|
||||
useProgramDataStore,
|
||||
} from './src/store/ProgramDataStore.store'
|
||||
|
||||
export type * from './src/types'
|
||||
Reference in New Issue
Block a user