feat(globalStateStore): implement global state management with persistence
- refactor state structure to include app, layout, navigation, owner, program, session, and user - add slices for managing program, session, owner, user, layout, navigation, and app states - create context provider for global state with automatic fetching and throttling - implement persistence using IndexedDB with localStorage fallback - add comprehensive README documentation for usage and API
This commit is contained in:
@@ -1,109 +1,93 @@
|
||||
import { createStore, entries, set, type UseStore } from 'idb-keyval';
|
||||
import { get, set } from 'idb-keyval';
|
||||
|
||||
import type { GlobalState } from './GlobalStateStore.types';
|
||||
|
||||
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 SKIP_PATHS = new Set([
|
||||
'app.controls',
|
||||
'session.connected',
|
||||
'session.error',
|
||||
'session.loading',
|
||||
]);
|
||||
|
||||
const shouldSkipPath = (path: string): boolean => {
|
||||
return SKIP_PATHS.has(path);
|
||||
};
|
||||
|
||||
const programDataIndexDBStore: null | UseStore = initilizeStore();
|
||||
|
||||
const skipKeysCallback = (dataKey: string, dataValue: any) => {
|
||||
if (typeof dataValue === 'function') {
|
||||
const filterState = (state: unknown, prefix = ''): unknown => {
|
||||
if (typeof state === 'function') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (
|
||||
dataKey === 'loading' ||
|
||||
dataKey === 'error' ||
|
||||
dataKey === 'security' ||
|
||||
dataKey === 'meta' ||
|
||||
dataKey === 'help'
|
||||
) {
|
||||
return undefined;
|
||||
if (state === null || typeof state !== 'object') {
|
||||
return state;
|
||||
}
|
||||
|
||||
return dataValue;
|
||||
if (Array.isArray(state)) {
|
||||
return state.map((item, idx) => filterState(item, `${prefix}[${idx}]`));
|
||||
}
|
||||
|
||||
const filtered: Record<string, unknown> = {};
|
||||
for (const [key, value] of Object.entries(state)) {
|
||||
const path = prefix ? `${prefix}.${key}` : key;
|
||||
|
||||
if (shouldSkipPath(path) || typeof value === 'function') {
|
||||
continue;
|
||||
}
|
||||
|
||||
filtered[key] = filterState(value, path);
|
||||
}
|
||||
|
||||
return filtered;
|
||||
};
|
||||
|
||||
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;
|
||||
async function loadStorage(): Promise<Partial<GlobalState>> {
|
||||
try {
|
||||
if (typeof indexedDB !== 'undefined') {
|
||||
const data = await get(STORAGE_KEY);
|
||||
if (data) {
|
||||
return JSON.parse(data) as Partial<GlobalState>;
|
||||
}
|
||||
return {} as T;
|
||||
} catch (e) {
|
||||
console.error('Failed to load storage: ', storageKey ?? STORAGE_KEY, e);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load from IndexedDB, falling back to localStorage:', e);
|
||||
}
|
||||
|
||||
return {} as T;
|
||||
try {
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
const data = localStorage.getItem(STORAGE_KEY);
|
||||
if (data) {
|
||||
return JSON.parse(data) as Partial<GlobalState>;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load from localStorage:', e);
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
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);
|
||||
async function saveStorage(state: GlobalState): Promise<void> {
|
||||
const filtered = filterState(state);
|
||||
const serialized = JSON.stringify(filtered);
|
||||
|
||||
localStorage.setItem(storageKey ?? STORAGE_KEY, dataString ?? '{}');
|
||||
return data;
|
||||
} catch (e) {
|
||||
console.error('Failed to save localStorage storage: ', storageKey ?? STORAGE_KEY, e);
|
||||
try {
|
||||
if (typeof indexedDB !== 'undefined') {
|
||||
await set(STORAGE_KEY, serialized);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to save to IndexedDB, falling back to localStorage:', e);
|
||||
}
|
||||
|
||||
try {
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.setItem(STORAGE_KEY, serialized);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to save to localStorage:', e);
|
||||
}
|
||||
return {} as T;
|
||||
}
|
||||
|
||||
export { loadStorage, saveStorage };
|
||||
|
||||
Reference in New Issue
Block a user