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:
2026-02-07 20:03:27 +02:00
parent 202a826642
commit f737b1d11d
22 changed files with 3098 additions and 488 deletions

View File

@@ -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 };