Compare commits
2 Commits
c2113357f2
...
f365d7b0e0
| Author | SHA1 | Date | |
|---|---|---|---|
| f365d7b0e0 | |||
| 210a1d44e7 |
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json",
|
"$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json",
|
||||||
"changelog": "@changesets/changelog-git",
|
"changelog": "@changesets/changelog-git",
|
||||||
"commit": false,
|
"commit": true,
|
||||||
"fixed": [],
|
"fixed": [],
|
||||||
"linked": [],
|
"linked": [],
|
||||||
"access": "public",
|
"access": "public",
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
# @warkypublic/zustandsyncstore
|
# @warkypublic/zustandsyncstore
|
||||||
|
|
||||||
|
## 0.0.38
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- 210a1d4: feat(GlobalStateStore): add initialization state and update actions
|
||||||
|
|
||||||
## 0.0.37
|
## 0.0.37
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@warkypublic/oranguru",
|
"name": "@warkypublic/oranguru",
|
||||||
"author": "Warky Devs",
|
"author": "Warky Devs",
|
||||||
"version": "0.0.37",
|
"version": "0.0.38",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"types": "./dist/lib.d.ts",
|
"types": "./dist/lib.d.ts",
|
||||||
"main": "./dist/lib.cjs.js",
|
"main": "./dist/lib.cjs.js",
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import type {
|
|||||||
import { loadStorage, saveStorage } from './GlobalStateStore.utils';
|
import { loadStorage, saveStorage } from './GlobalStateStore.utils';
|
||||||
|
|
||||||
const initialState: GlobalState = {
|
const initialState: GlobalState = {
|
||||||
|
initialized: false,
|
||||||
layout: {
|
layout: {
|
||||||
bottomBar: { open: false },
|
bottomBar: { open: false },
|
||||||
leftBar: { open: false },
|
leftBar: { open: false },
|
||||||
@@ -141,13 +142,14 @@ const createNavigationSlice = (set: SetState) => ({
|
|||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
|
|
||||||
const createComplexActions = (set: SetState, get: GetState) => ({
|
const createComplexActions = (set: SetState, get: GetState) => {
|
||||||
fetchData: async (url?: string) => {
|
// Internal implementation without lock
|
||||||
|
const fetchDataInternal = async (url?: string) => {
|
||||||
try {
|
try {
|
||||||
set((state: GlobalState) => ({
|
set((state: GlobalState) => ({
|
||||||
session: {
|
session: {
|
||||||
...state.session,
|
...state.session,
|
||||||
apiURL: url ?? state.session.apiURL,
|
apiURL: url || state.session.apiURL,
|
||||||
loading: true,
|
loading: true,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
@@ -190,7 +192,16 @@ 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 => {
|
isLoggedIn: (): boolean => {
|
||||||
const session = get().session;
|
const session = get().session;
|
||||||
@@ -203,92 +214,146 @@ const createComplexActions = (set: SetState, get: GetState) => ({
|
|||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
|
||||||
login: async (authToken?: string) => {
|
login: async (authToken?: string, user?: Partial<UserState>) => {
|
||||||
try {
|
// Wait for initialization to complete
|
||||||
set((state: GlobalState) => ({
|
await waitForInitialization();
|
||||||
session: {
|
|
||||||
...state.session,
|
|
||||||
authToken: authToken ?? '',
|
|
||||||
expiryDate: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
loading: true,
|
|
||||||
loggedIn: true,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
await get().fetchData();
|
|
||||||
const currentState = get();
|
|
||||||
const result = await currentState.onLogin?.(currentState);
|
|
||||||
if (result) {
|
|
||||||
set((state: GlobalState) => ({
|
|
||||||
...state,
|
|
||||||
owner: result.owner ? { ...state.owner, ...result.owner } : state.owner,
|
|
||||||
program: result.program ? { ...state.program, ...result.program } : state.program,
|
|
||||||
session: result.session ? { ...state.session, ...result.session } : state.session,
|
|
||||||
user: result.user ? { ...state.user, ...result.user } : state.user,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
set((state: GlobalState) => ({
|
|
||||||
session: {
|
|
||||||
...state.session,
|
|
||||||
connected: false,
|
|
||||||
error: `Login Exception: ${String(e)}`,
|
|
||||||
loading: false,
|
|
||||||
loggedIn: false,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
} finally {
|
|
||||||
set((state: GlobalState) => ({
|
|
||||||
session: {
|
|
||||||
...state.session,
|
|
||||||
loading: false,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
logout: async () => {
|
// Use lock to prevent concurrent auth operations
|
||||||
try {
|
return withOperationLock(async () => {
|
||||||
set((state: GlobalState) => ({
|
try {
|
||||||
...initialState,
|
set((state: GlobalState) => ({
|
||||||
session: {
|
session: {
|
||||||
...initialState.session,
|
...state.session,
|
||||||
apiURL: state.session.apiURL,
|
authToken: authToken ?? '',
|
||||||
expiryDate: undefined,
|
expiryDate: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
|
||||||
loading: true,
|
loading: true,
|
||||||
loggedIn: false,
|
loggedIn: true,
|
||||||
},
|
},
|
||||||
}));
|
user: {
|
||||||
await get().fetchData();
|
...state.user,
|
||||||
const currentState = get();
|
...user,
|
||||||
const result = await currentState.onLogout?.(currentState);
|
},
|
||||||
if (result) {
|
}));
|
||||||
set((state: GlobalState) => ({
|
|
||||||
...state,
|
const currentState = get();
|
||||||
owner: result.owner ? { ...state.owner, ...result.owner } : state.owner,
|
const result = await currentState.onLogin?.(currentState);
|
||||||
program: result.program ? { ...state.program, ...result.program } : state.program,
|
if (result) {
|
||||||
session: result.session ? { ...state.session, ...result.session } : state.session,
|
set((state: GlobalState) => ({
|
||||||
user: result.user ? { ...state.user, ...result.user } : state.user,
|
...state,
|
||||||
}));
|
owner: result.owner ? { ...state.owner, ...result.owner } : state.owner,
|
||||||
}
|
program: result.program ? { ...state.program, ...result.program } : state.program,
|
||||||
} catch (e) {
|
session: result.session ? { ...state.session, ...result.session } : state.session,
|
||||||
set((state: GlobalState) => ({
|
user: result.user ? { ...state.user, ...result.user } : state.user,
|
||||||
session: {
|
}));
|
||||||
...state.session,
|
}
|
||||||
connected: false,
|
// Call internal version to avoid nested lock
|
||||||
error: `Logout Exception: ${String(e)}`,
|
await fetchDataInternal();
|
||||||
loading: false,
|
} catch (e) {
|
||||||
},
|
set((state: GlobalState) => ({
|
||||||
}));
|
session: {
|
||||||
} finally {
|
...state.session,
|
||||||
set((state: GlobalState) => ({
|
connected: false,
|
||||||
session: {
|
error: `Login Exception: ${String(e)}`,
|
||||||
...state.session,
|
loading: false,
|
||||||
loading: false,
|
loggedIn: false,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
}
|
} finally {
|
||||||
},
|
set((state: GlobalState) => ({
|
||||||
});
|
session: {
|
||||||
|
...state.session,
|
||||||
|
loading: false,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
logout: async () => {
|
||||||
|
// Wait for initialization to complete
|
||||||
|
await waitForInitialization();
|
||||||
|
|
||||||
|
// Use lock to prevent concurrent auth operations
|
||||||
|
return withOperationLock(async () => {
|
||||||
|
try {
|
||||||
|
set((state: GlobalState) => ({
|
||||||
|
...initialState,
|
||||||
|
session: {
|
||||||
|
...initialState.session,
|
||||||
|
apiURL: state.session.apiURL,
|
||||||
|
expiryDate: undefined,
|
||||||
|
loading: true,
|
||||||
|
loggedIn: false,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const currentState = get();
|
||||||
|
const result = await currentState.onLogout?.(currentState);
|
||||||
|
if (result) {
|
||||||
|
set((state: GlobalState) => ({
|
||||||
|
...state,
|
||||||
|
owner: result.owner ? { ...state.owner, ...result.owner } : state.owner,
|
||||||
|
program: result.program ? { ...state.program, ...result.program } : state.program,
|
||||||
|
session: result.session ? { ...state.session, ...result.session } : state.session,
|
||||||
|
user: result.user ? { ...state.user, ...result.user } : state.user,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
// Call internal version to avoid nested lock
|
||||||
|
await fetchDataInternal();
|
||||||
|
} catch (e) {
|
||||||
|
set((state: GlobalState) => ({
|
||||||
|
session: {
|
||||||
|
...state.session,
|
||||||
|
connected: false,
|
||||||
|
error: `Logout Exception: ${String(e)}`,
|
||||||
|
loading: false,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
} finally {
|
||||||
|
set((state: GlobalState) => ({
|
||||||
|
session: {
|
||||||
|
...state.session,
|
||||||
|
loading: false,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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) => ({
|
const GlobalStateStore = createStore<GlobalStateStoreType>((set, get) => ({
|
||||||
...initialState,
|
...initialState,
|
||||||
@@ -301,33 +366,40 @@ const GlobalStateStore = createStore<GlobalStateStoreType>((set, get) => ({
|
|||||||
...createComplexActions(set, get),
|
...createComplexActions(set, get),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Flag to prevent saving during initial load
|
// Initialize storage and load saved state
|
||||||
let isInitialLoad = true;
|
initializationPromise = loadStorage()
|
||||||
|
|
||||||
loadStorage()
|
|
||||||
.then((state) => {
|
.then((state) => {
|
||||||
GlobalStateStore.setState((current) => ({
|
// Merge loaded state with initial state
|
||||||
...current,
|
GlobalStateStore.setState(
|
||||||
...state,
|
(current) => ({
|
||||||
session: {
|
...current,
|
||||||
...current.session,
|
...state,
|
||||||
...state.session,
|
initialized: true,
|
||||||
connected: true,
|
session: {
|
||||||
loading: false,
|
...current.session,
|
||||||
},
|
...state.session,
|
||||||
}));
|
connected: true,
|
||||||
|
loading: false,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
true // Replace state completely to avoid triggering subscription during init
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
console.error('Error loading storage:', e);
|
console.error('Error loading storage:', e);
|
||||||
|
// Mark as initialized even on error so app doesn't hang
|
||||||
|
GlobalStateStore.setState({ initialized: true });
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
// Enable saving after initial load completes
|
// Mark initialization as complete
|
||||||
isInitialLoad = false;
|
isStorageInitialized = true;
|
||||||
|
initializationPromise = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Subscribe to state changes and persist to storage
|
||||||
|
// Only saves after initialization is complete
|
||||||
GlobalStateStore.subscribe((state) => {
|
GlobalStateStore.subscribe((state) => {
|
||||||
// Skip saving during initial load
|
if (!isStorageInitialized) {
|
||||||
if (isInitialLoad) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ type DatabaseDetail = {
|
|||||||
type ExtractState<S> = S extends { getState: () => infer X } ? X : never;
|
type ExtractState<S> = S extends { getState: () => infer X } ? X : never;
|
||||||
|
|
||||||
interface GlobalState {
|
interface GlobalState {
|
||||||
|
initialized: boolean;
|
||||||
layout: LayoutState;
|
layout: LayoutState;
|
||||||
navigation: NavigationState;
|
navigation: NavigationState;
|
||||||
owner: OwnerState;
|
owner: OwnerState;
|
||||||
@@ -31,7 +32,7 @@ interface GlobalStateActions {
|
|||||||
fetchData: (url?: string) => Promise<void>;
|
fetchData: (url?: string) => Promise<void>;
|
||||||
|
|
||||||
isLoggedIn: () => boolean;
|
isLoggedIn: () => boolean;
|
||||||
login: (authToken?: string) => Promise<void>;
|
login: (authToken?: string, user?: Partial<UserState>) => Promise<void>;
|
||||||
logout: () => Promise<void>;
|
logout: () => Promise<void>;
|
||||||
// Callbacks for custom logic
|
// Callbacks for custom logic
|
||||||
onFetchSession?: (state: Partial<GlobalState>) => Promise<Partial<GlobalState>>;
|
onFetchSession?: (state: Partial<GlobalState>) => Promise<Partial<GlobalState>>;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { get, set } from 'idb-keyval';
|
|||||||
|
|
||||||
describe('GlobalStateStore.utils', () => {
|
describe('GlobalStateStore.utils', () => {
|
||||||
const mockState: GlobalState = {
|
const mockState: GlobalState = {
|
||||||
|
initialized: false,
|
||||||
layout: {
|
layout: {
|
||||||
bottomBar: { open: false },
|
bottomBar: { open: false },
|
||||||
leftBar: { open: false },
|
leftBar: { open: false },
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const STORAGE_KEY = 'APP_GLO';
|
|||||||
|
|
||||||
const SKIP_PATHS = new Set([
|
const SKIP_PATHS = new Set([
|
||||||
'app.controls',
|
'app.controls',
|
||||||
|
'initialized',
|
||||||
'session.connected',
|
'session.connected',
|
||||||
'session.error',
|
'session.error',
|
||||||
'session.loading',
|
'session.loading',
|
||||||
|
|||||||
Reference in New Issue
Block a user