docs(changeset): feat(GlobalStateStore): add initialization state and update actions
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json",
|
||||
"changelog": "@changesets/changelog-git",
|
||||
"commit": false,
|
||||
"commit": true,
|
||||
"fixed": [],
|
||||
"linked": [],
|
||||
"access": "public",
|
||||
|
||||
5
.changeset/puny-pots-crash.md
Normal file
5
.changeset/puny-pots-crash.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@warkypublic/oranguru': patch
|
||||
---
|
||||
|
||||
feat(GlobalStateStore): add initialization state and update actions
|
||||
@@ -20,6 +20,7 @@ import type {
|
||||
import { loadStorage, saveStorage } from './GlobalStateStore.utils';
|
||||
|
||||
const initialState: GlobalState = {
|
||||
initialized: false,
|
||||
layout: {
|
||||
bottomBar: { open: false },
|
||||
leftBar: { open: false },
|
||||
@@ -141,13 +142,14 @@ const createNavigationSlice = (set: SetState) => ({
|
||||
})),
|
||||
});
|
||||
|
||||
const createComplexActions = (set: SetState, get: GetState) => ({
|
||||
fetchData: async (url?: string) => {
|
||||
const createComplexActions = (set: SetState, get: GetState) => {
|
||||
// Internal implementation without lock
|
||||
const fetchDataInternal = async (url?: string) => {
|
||||
try {
|
||||
set((state: GlobalState) => ({
|
||||
session: {
|
||||
...state.session,
|
||||
apiURL: url ?? state.session.apiURL,
|
||||
apiURL: url || state.session.apiURL,
|
||||
loading: true,
|
||||
},
|
||||
}));
|
||||
@@ -190,6 +192,15 @@ const createComplexActions = (set: SetState, get: GetState) => ({
|
||||
},
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
fetchData: async (url?: string) => {
|
||||
// Wait for initialization to complete
|
||||
await waitForInitialization();
|
||||
|
||||
// Use lock to prevent concurrent fetchData calls
|
||||
return withOperationLock(() => fetchDataInternal(url));
|
||||
},
|
||||
|
||||
isLoggedIn: (): boolean => {
|
||||
@@ -203,7 +214,12 @@ const createComplexActions = (set: SetState, get: GetState) => ({
|
||||
return true;
|
||||
},
|
||||
|
||||
login: async (authToken?: string) => {
|
||||
login: async (authToken?: string, user?: Partial<UserState>) => {
|
||||
// Wait for initialization to complete
|
||||
await waitForInitialization();
|
||||
|
||||
// Use lock to prevent concurrent auth operations
|
||||
return withOperationLock(async () => {
|
||||
try {
|
||||
set((state: GlobalState) => ({
|
||||
session: {
|
||||
@@ -213,8 +229,12 @@ const createComplexActions = (set: SetState, get: GetState) => ({
|
||||
loading: true,
|
||||
loggedIn: true,
|
||||
},
|
||||
user: {
|
||||
...state.user,
|
||||
...user,
|
||||
},
|
||||
}));
|
||||
await get().fetchData();
|
||||
|
||||
const currentState = get();
|
||||
const result = await currentState.onLogin?.(currentState);
|
||||
if (result) {
|
||||
@@ -226,6 +246,8 @@ const createComplexActions = (set: SetState, get: GetState) => ({
|
||||
user: result.user ? { ...state.user, ...result.user } : state.user,
|
||||
}));
|
||||
}
|
||||
// Call internal version to avoid nested lock
|
||||
await fetchDataInternal();
|
||||
} catch (e) {
|
||||
set((state: GlobalState) => ({
|
||||
session: {
|
||||
@@ -244,9 +266,15 @@ const createComplexActions = (set: SetState, get: GetState) => ({
|
||||
},
|
||||
}));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
logout: async () => {
|
||||
// Wait for initialization to complete
|
||||
await waitForInitialization();
|
||||
|
||||
// Use lock to prevent concurrent auth operations
|
||||
return withOperationLock(async () => {
|
||||
try {
|
||||
set((state: GlobalState) => ({
|
||||
...initialState,
|
||||
@@ -258,7 +286,7 @@ const createComplexActions = (set: SetState, get: GetState) => ({
|
||||
loggedIn: false,
|
||||
},
|
||||
}));
|
||||
await get().fetchData();
|
||||
|
||||
const currentState = get();
|
||||
const result = await currentState.onLogout?.(currentState);
|
||||
if (result) {
|
||||
@@ -270,6 +298,8 @@ const createComplexActions = (set: SetState, get: GetState) => ({
|
||||
user: result.user ? { ...state.user, ...result.user } : state.user,
|
||||
}));
|
||||
}
|
||||
// Call internal version to avoid nested lock
|
||||
await fetchDataInternal();
|
||||
} catch (e) {
|
||||
set((state: GlobalState) => ({
|
||||
session: {
|
||||
@@ -287,8 +317,43 @@ const createComplexActions = (set: SetState, get: GetState) => ({
|
||||
},
|
||||
}));
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
// State management flags and locks - must be defined before store creation
|
||||
let isStorageInitialized = false;
|
||||
let initializationPromise: null | Promise<void> = null;
|
||||
let operationLock: Promise<void> = Promise.resolve();
|
||||
|
||||
// Helper to wait for initialization - must be defined before store creation
|
||||
const waitForInitialization = async (): Promise<void> => {
|
||||
if (initializationPromise) {
|
||||
await initializationPromise;
|
||||
}
|
||||
};
|
||||
|
||||
// Helper to ensure async operations run sequentially
|
||||
const withOperationLock = async <T>(operation: () => Promise<T>): Promise<T> => {
|
||||
const currentLock = operationLock;
|
||||
let releaseLock: () => void;
|
||||
|
||||
// Create new lock promise
|
||||
operationLock = new Promise<void>((resolve) => {
|
||||
releaseLock = resolve;
|
||||
});
|
||||
|
||||
try {
|
||||
// Wait for previous operation to complete
|
||||
await currentLock;
|
||||
// Run the operation
|
||||
return await operation();
|
||||
} finally {
|
||||
// Release the lock
|
||||
releaseLock!();
|
||||
}
|
||||
};
|
||||
|
||||
const GlobalStateStore = createStore<GlobalStateStoreType>((set, get) => ({
|
||||
...initialState,
|
||||
@@ -301,33 +366,40 @@ const GlobalStateStore = createStore<GlobalStateStoreType>((set, get) => ({
|
||||
...createComplexActions(set, get),
|
||||
}));
|
||||
|
||||
// Flag to prevent saving during initial load
|
||||
let isInitialLoad = true;
|
||||
|
||||
loadStorage()
|
||||
// Initialize storage and load saved state
|
||||
initializationPromise = loadStorage()
|
||||
.then((state) => {
|
||||
GlobalStateStore.setState((current) => ({
|
||||
// Merge loaded state with initial state
|
||||
GlobalStateStore.setState(
|
||||
(current) => ({
|
||||
...current,
|
||||
...state,
|
||||
initialized: true,
|
||||
session: {
|
||||
...current.session,
|
||||
...state.session,
|
||||
connected: true,
|
||||
loading: false,
|
||||
},
|
||||
}));
|
||||
}),
|
||||
true // Replace state completely to avoid triggering subscription during init
|
||||
);
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error('Error loading storage:', e);
|
||||
// Mark as initialized even on error so app doesn't hang
|
||||
GlobalStateStore.setState({ initialized: true });
|
||||
})
|
||||
.finally(() => {
|
||||
// Enable saving after initial load completes
|
||||
isInitialLoad = false;
|
||||
// Mark initialization as complete
|
||||
isStorageInitialized = true;
|
||||
initializationPromise = null;
|
||||
});
|
||||
|
||||
// Subscribe to state changes and persist to storage
|
||||
// Only saves after initialization is complete
|
||||
GlobalStateStore.subscribe((state) => {
|
||||
// Skip saving during initial load
|
||||
if (isInitialLoad) {
|
||||
if (!isStorageInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ type DatabaseDetail = {
|
||||
type ExtractState<S> = S extends { getState: () => infer X } ? X : never;
|
||||
|
||||
interface GlobalState {
|
||||
initialized: boolean;
|
||||
layout: LayoutState;
|
||||
navigation: NavigationState;
|
||||
owner: OwnerState;
|
||||
@@ -31,7 +32,7 @@ interface GlobalStateActions {
|
||||
fetchData: (url?: string) => Promise<void>;
|
||||
|
||||
isLoggedIn: () => boolean;
|
||||
login: (authToken?: string) => Promise<void>;
|
||||
login: (authToken?: string, user?: Partial<UserState>) => Promise<void>;
|
||||
logout: () => Promise<void>;
|
||||
// Callbacks for custom logic
|
||||
onFetchSession?: (state: Partial<GlobalState>) => Promise<Partial<GlobalState>>;
|
||||
|
||||
@@ -14,6 +14,7 @@ import { get, set } from 'idb-keyval';
|
||||
|
||||
describe('GlobalStateStore.utils', () => {
|
||||
const mockState: GlobalState = {
|
||||
initialized: false,
|
||||
layout: {
|
||||
bottomBar: { open: false },
|
||||
leftBar: { open: false },
|
||||
|
||||
@@ -6,6 +6,7 @@ const STORAGE_KEY = 'APP_GLO';
|
||||
|
||||
const SKIP_PATHS = new Set([
|
||||
'app.controls',
|
||||
'initialized',
|
||||
'session.connected',
|
||||
'session.error',
|
||||
'session.loading',
|
||||
|
||||
Reference in New Issue
Block a user