diff --git a/packages/clerk-js/src/core/cacheClearManager.ts b/packages/clerk-js/src/core/cacheClearManager.ts new file mode 100644 index 00000000000..c43385a631c --- /dev/null +++ b/packages/clerk-js/src/core/cacheClearManager.ts @@ -0,0 +1,28 @@ +import { debugLogger } from '@/utils/debug'; + +const clearCallbacks = new Map void>(); + +/** + * Registers a callback to be executed when caches are cleared. + * @param id - Unique identifier for the callback + * @param callback - Function to execute when clearing caches + * @returns Function to unregister the callback + */ +export const registerClearCallback = (id: string, callback: () => void): (() => void) => { + clearCallbacks.set(id, callback); + return () => clearCallbacks.delete(id); +}; + +/** + * Clears all registered caches by executing all registered callbacks. + * Continues execution even if individual callbacks throw errors. + */ +export const clearAllCaches = (): void => { + clearCallbacks.forEach(callback => { + try { + callback(); + } catch (error) { + debugLogger.error('Error executing clear callback', { error }, 'CacheClearManager'); + } + }); +}; diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 807942113bf..7cab3347919 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -525,6 +525,15 @@ export class Clerk implements ClerkInterface { // Notify other tabs that user is signing out and clean up cookies. eventBus.emit(events.UserSignOut, null); + if (typeof window !== 'undefined') { + try { + const { clearAllCaches } = await import('./cacheClearManager'); + clearAllCaches(); + } catch (error) { + debugLogger.debug('CacheClearManager not available during signOut, skipping cache clear', { error }, 'clerk'); + } + } + this.#setTransitiveState(); await tracker.track(async () => { diff --git a/packages/clerk-js/src/core/requestCache.ts b/packages/clerk-js/src/core/requestCache.ts new file mode 100644 index 00000000000..e102e47d5b8 --- /dev/null +++ b/packages/clerk-js/src/core/requestCache.ts @@ -0,0 +1,61 @@ +import { debugLogger } from '@/utils/debug'; + +export type RequestCacheState = { + data: T | null; + error: Error | null; + isLoading: boolean; + isValidating: boolean; + cachedAt?: number; +}; + +const cache = new Map>(); +const subscribers = new Set<() => void>(); + +/** + * Gets a cache entry + */ +export const getCacheEntry = (key: string): RequestCacheState | undefined => { + return cache.get(key) as RequestCacheState | undefined; +}; + +/** + * Sets a cache entry and notifies subscribers + */ +export const setCacheEntry = ( + key: string, + state: RequestCacheState | ((current: RequestCacheState | undefined) => RequestCacheState), +): void => { + const currentState = cache.get(key) as RequestCacheState | undefined; + const newState = typeof state === 'function' ? state(currentState) : state; + cache.set(key, newState as RequestCacheState); + notifySubscribers(); +}; + +/** + * Subscribes to cache changes + */ +export const subscribe = (callback: () => void): (() => void) => { + subscribers.add(callback); + return () => subscribers.delete(callback); +}; + +/** + * Clears the entire request cache + */ +export const clearRequestCache = (): void => { + cache.clear(); + notifySubscribers(); +}; + +/** + * Notifies all subscribers of cache changes + */ +const notifySubscribers = (): void => { + subscribers.forEach(callback => { + try { + callback(); + } catch (error) { + debugLogger.error('Error executing subscriber callback', { error }, 'RequestCache'); + } + }); +}; diff --git a/packages/clerk-js/src/ui/hooks/useFetch.ts b/packages/clerk-js/src/ui/hooks/useFetch.ts index 7abe9207c49..e13b795b325 100644 --- a/packages/clerk-js/src/ui/hooks/useFetch.ts +++ b/packages/clerk-js/src/ui/hooks/useFetch.ts @@ -1,54 +1,43 @@ import { useCallback, useEffect, useRef, useSyncExternalStore } from 'react'; -export type State = { - data: Data | null; - error: Error | null; - /** - * if there's an ongoing request and no "loaded data" - */ - isLoading: boolean; - /** - * if there's a request or revalidation loading - */ - isValidating: boolean; - cachedAt?: number; -}; - -/** - * Global cache for storing status of fetched resources - */ -let requestCache = new Map(); - -/** - * A set to store subscribers in order to notify when the value of a key of `requestCache` changes - */ -const subscribers = new Set<() => void>(); +import { registerClearCallback } from '@/core/cacheClearManager'; +import { + clearRequestCache, + getCacheEntry, + type RequestCacheState, + setCacheEntry, + subscribe, +} from '@/core/requestCache'; /** * This utility should only be used in tests to clear previously fetched data */ export const clearFetchCache = () => { - requestCache = new Map(); + clearRequestCache(); }; +if (typeof window !== 'undefined') { + registerClearCallback('useFetch', clearFetchCache); +} + const serialize = (key: unknown) => (typeof key === 'string' ? key : JSON.stringify(key)); const useCache = ( key: K, serializer = serialize, ): { - getCache: () => State | undefined; - setCache: (state: State | ((params: State) => State)) => void; + getCache: () => RequestCacheState | undefined; + setCache: ( + state: RequestCacheState | ((params: RequestCacheState | undefined) => RequestCacheState), + ) => void; clearCache: () => void; subscribeCache: (callback: () => void) => () => void; } => { const serializedKey = serializer(key); - const get = useCallback(() => requestCache.get(serializedKey), [serializedKey]); + const get = useCallback(() => getCacheEntry(serializedKey), [serializedKey]); const set = useCallback( - (data: State | ((params: State) => State)) => { - // @ts-ignore - requestCache.set(serializedKey, typeof data === 'function' ? data(get()) : data); - subscribers.forEach(callback => callback()); + (data: RequestCacheState | ((params: RequestCacheState | undefined) => RequestCacheState)) => { + setCacheEntry(serializedKey, data); }, [serializedKey], ); @@ -62,15 +51,14 @@ const useCache = ( cachedAt: undefined, }); }, [set]); - const subscribe = useCallback((callback: () => void) => { - subscribers.add(callback); - return () => subscribers.delete(callback); + const subscribeCache = useCallback((callback: () => void) => { + return subscribe(callback); }, []); return { getCache: get, setCache: set, - subscribeCache: subscribe, + subscribeCache, clearCache: clear, }; }; @@ -116,11 +104,15 @@ export const useFetch = ( const revalidateCache = useSyncExternalStore(subscribeRevalidationCounter, getRevalidationCounter); const revalidate = useCallback(() => { - setCache(d => ({ + setCache((d: RequestCacheState | undefined) => ({ + data: null, + error: null, + isLoading: false, + isValidating: false, ...d, cachedAt: 0, })); - setRevalidationCounter(d => ({ + setRevalidationCounter((d: RequestCacheState | undefined) => ({ isLoading: false, isValidating: false, error: null,