fix: metadata store lifecycle during sign-in, sign-out, and locale change (#18901)
## Summary Fixes metadata lifecycle bugs during sign-in, sign-out, and locale change: - **Cross-tab sign-out**: Broadcasts sign-out via `BroadcastChannel` so other tabs clear their session gracefully instead of hitting stale-token errors - **SSE teardown on sign-out**: Handles `UNAUTHENTICATED`/`FORBIDDEN` errors in the SSE event stream effect instead of throwing unhandled errors - **Locale switch resilience**: `invalidateAndReload` now invalidates collection hashes instead of clearing the store to empty, so components never see 0 metadata items during the reload transition - **Sign-in background mock**: Uses non-throwing `objectMetadataItemFamilySelector` instead of hooks that throw on missing metadata - **View name placeholders**: Guards against `undefined` `viewName` during metadata transitions (the minimal metadata query doesn't include `name`) - **Session cleanup**: Selective `localStorage` clearing (`clearSessionLocalStorageKeys`) preserves metadata keys; `clearAllSessionLocalStorageKeys` for full clears - **Metadata reload API**: New `useMetadataStoreActions` hook as the high-level API for metadata lifecycle operations (`applyMockedMetadata`, `invalidateAndReload`, `loadMockedMetadataAtomic`) ## Test plan - [ ] Sign out on Tab A → Tab A shows sign-in page with no console errors - [ ] Tab B (logged in) receives cross-tab broadcast and redirects to sign-in - [ ] No "Forbidden resource" SSE errors in console during sign-out - [ ] Change language in Settings > Experience → no crash, metadata refreshes in background - [ ] Sign back in after sign-out → metadata loads correctly, app is functional - [ ] Re-sign-in after locale change → correct locale is preserved
This commit is contained in:
+24
-8
@@ -2,8 +2,9 @@ import { useHasAccessTokenPair } from '@/auth/hooks/useHasAccessTokenPair';
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { isCurrentUserLoadedState } from '@/auth/states/isCurrentUserLoadedState';
|
||||
import { useLoadMinimalMetadata } from '@/metadata-store/hooks/useLoadMinimalMetadata';
|
||||
import { useLoadMockedMinimalMetadata } from '@/metadata-store/hooks/useLoadMockedMinimalMetadata';
|
||||
import { useLoadMockedMetadata } from '@/metadata-store/hooks/useLoadMockedMetadata';
|
||||
import { useLoadStaleMetadataEntities } from '@/metadata-store/hooks/useLoadStaleMetadataEntities';
|
||||
import { metadataLoadedVersionState } from '@/metadata-store/states/metadataLoadedVersionState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { isWorkspaceActiveOrSuspended } from 'twenty-shared/workspace';
|
||||
@@ -25,10 +26,14 @@ export const MinimalMetadataLoadEffect = () => {
|
||||
const hasAccessTokenPair = useHasAccessTokenPair();
|
||||
const isCurrentUserLoaded = useAtomStateValue(isCurrentUserLoadedState);
|
||||
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
|
||||
const [loadedState, setLoadedState] = useState<LoadedState>('none');
|
||||
const metadataLoadedVersion = useAtomStateValue(metadataLoadedVersionState);
|
||||
const [lastMetadataLoadData, setLastMetadataLoadData] = useState<{
|
||||
state: LoadedState;
|
||||
version: number;
|
||||
}>({ state: 'none', version: -1 });
|
||||
|
||||
const { loadMinimalMetadata } = useLoadMinimalMetadata();
|
||||
const { loadMockedMinimalMetadata } = useLoadMockedMinimalMetadata();
|
||||
const { loadMockedMetadataAtomic } = useLoadMockedMetadata();
|
||||
const { loadStaleMetadataEntities } = useLoadStaleMetadataEntities();
|
||||
|
||||
const isActiveWorkspace = isWorkspaceActiveOrSuspended(currentWorkspace);
|
||||
@@ -39,15 +44,25 @@ export const MinimalMetadataLoadEffect = () => {
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isCurrentUserLoaded || loadedState === desiredLoadState) {
|
||||
if (!isCurrentUserLoaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLoadedState(desiredLoadState);
|
||||
const versionChanged =
|
||||
metadataLoadedVersion !== lastMetadataLoadData.version;
|
||||
|
||||
if (!versionChanged && lastMetadataLoadData.state === desiredLoadState) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLastMetadataLoadData({
|
||||
state: desiredLoadState,
|
||||
version: metadataLoadedVersion,
|
||||
});
|
||||
|
||||
const performLoad = async () => {
|
||||
if (desiredLoadState === 'mocked') {
|
||||
await loadMockedMinimalMetadata();
|
||||
await loadMockedMetadataAtomic();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -64,9 +79,10 @@ export const MinimalMetadataLoadEffect = () => {
|
||||
hasAccessTokenPair,
|
||||
isActiveWorkspace,
|
||||
desiredLoadState,
|
||||
loadedState,
|
||||
lastMetadataLoadData,
|
||||
metadataLoadedVersion,
|
||||
loadMinimalMetadata,
|
||||
loadMockedMinimalMetadata,
|
||||
loadMockedMetadataAtomic,
|
||||
loadStaleMetadataEntities,
|
||||
]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user