From 23a9a09a4de2cd61684382b13f654b5fa062b2bc Mon Sep 17 00:00:00 2001 From: Jacob Maynard Date: Fri, 26 Dec 2025 14:52:46 -0600 Subject: [PATCH 1/3] should prevent issues when loading website from cache --- packages/web/src/api/better-auth-store.js | 43 +++++++++++- packages/web/src/lib/bfcache-handler.js | 85 +++++++++++++++++++++++ packages/web/src/main.jsx | 6 ++ packages/web/src/stores/projectStore.js | 44 ++++++++++++ 4 files changed, 176 insertions(+), 2 deletions(-) create mode 100644 packages/web/src/lib/bfcache-handler.js diff --git a/packages/web/src/api/better-auth-store.js b/packages/web/src/api/better-auth-store.js index 73bfedf79..c3d4f302c 100644 --- a/packages/web/src/api/better-auth-store.js +++ b/packages/web/src/api/better-auth-store.js @@ -195,10 +195,34 @@ function createBetterAuthStore() { // Listen for tab visibility changes to refresh session if (typeof document !== 'undefined') { - const handleVisibilityChange = () => { + const handleVisibilityChange = async () => { if (document.visibilityState === 'visible' && !authLoading() && isOnline()) { // Refresh session when tab becomes visible (user might have verified email in another tab) - session().refetch?.(); + try { + await session().refetch?.(); + + // Wait a bit for session to update + await new Promise(resolve => setTimeout(resolve, 100)); + + // Validate and refresh project list if user is authenticated + const currentUser = user(); + if (currentUser?.id) { + // Validate project list cache against current user + projectStore.validateProjectListCache(currentUser.id); + + // Refresh project list to ensure it's current + try { + await projectStore.refreshProjectList(currentUser.id); + } catch (err) { + console.warn('[auth] Failed to refresh project list after visibility change:', err); + } + } else { + // User is not authenticated, clear project list + projectStore.clearProjectList(); + } + } catch (err) { + console.warn('[auth] Failed to refresh session on visibility change:', err); + } } }; @@ -652,6 +676,20 @@ function createBetterAuthStore() { } } + /** + * Force refresh the auth session + * Used when page is restored from bfcache to ensure session is current + * Always refreshes, even if session is already loaded + */ + async function forceRefreshSession() { + try { + await session().refetch?.(); + } catch (err) { + console.warn('Force session refresh failed:', err); + throw err; + } + } + async function deleteAccount() { try { setAuthError(null); @@ -718,6 +756,7 @@ function createBetterAuthStore() { // Utility/compatibility methods getCurrentUser, refreshAccessToken, + forceRefreshSession, sendEmailVerification, getPendingEmail, getAccessToken, diff --git a/packages/web/src/lib/bfcache-handler.js b/packages/web/src/lib/bfcache-handler.js new file mode 100644 index 000000000..e8d5c5106 --- /dev/null +++ b/packages/web/src/lib/bfcache-handler.js @@ -0,0 +1,85 @@ +/** + * Back-Forward Cache (bfcache) Handler + * + * Detects when a page is restored from the browser's back-forward cache + * and triggers state refresh to ensure auth session and project data are current. + */ + +import { useBetterAuth } from '@api/better-auth-store.js'; +import projectStore from '@/stores/projectStore.js'; + +/** + * Initialize bfcache restoration handler + * Should be called once on app initialization + */ +export function initBfcacheHandler() { + if (typeof window === 'undefined') return; + + // Get auth instance once (it's a singleton) + const auth = useBetterAuth(); + + const handlePageshow = async event => { + // event.persisted === true means the page was restored from bfcache + if (!event.persisted) return; + + console.log('[bfcache] Page restored from back-forward cache, refreshing state...'); + + // Wait for auth to finish loading if it's currently loading + // This ensures we have the current user before validating project cache + if (auth.authLoading()) { + // Wait for auth to complete (with timeout) + await new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject(new Error('Auth loading timeout')); + }, 5000); + + const checkAuth = () => { + if (!auth.authLoading()) { + clearTimeout(timeout); + resolve(); + } else { + setTimeout(checkAuth, 100); + } + }; + + checkAuth(); + }).catch(err => { + console.warn('[bfcache] Auth loading timeout:', err); + }); + } + + // Force refresh the auth session to ensure it's current + try { + await auth.forceRefreshSession(); + } catch (err) { + console.warn('[bfcache] Failed to refresh auth session:', err); + } + + // Wait a bit for session to update + await new Promise(resolve => setTimeout(resolve, 100)); + + // Validate and refresh project list if user is authenticated + const currentUser = auth.user(); + if (currentUser?.id) { + // Validate project list cache against current user + projectStore.validateProjectListCache(currentUser.id); + + // Refresh project list to ensure it's current + try { + await projectStore.refreshProjectList(currentUser.id); + } catch (err) { + console.warn('[bfcache] Failed to refresh project list:', err); + } + } else { + // User is not authenticated, clear project list + projectStore.clearProjectList(); + } + }; + + window.addEventListener('pageshow', handlePageshow); + + // Return cleanup function (though typically not needed for singleton) + return () => { + window.removeEventListener('pageshow', handlePageshow); + }; +} diff --git a/packages/web/src/main.jsx b/packages/web/src/main.jsx index e4a51d4c2..7bffb8506 100644 --- a/packages/web/src/main.jsx +++ b/packages/web/src/main.jsx @@ -2,6 +2,7 @@ import { render } from 'solid-js/web'; import './global.css'; import Routes from './Routes.jsx'; import { cleanupExpiredStates } from '@lib/formStatePersistence.js'; +import { initBfcacheHandler } from '@lib/bfcache-handler.js'; import AppErrorBoundary from './components/ErrorBoundary.jsx'; // Clean up any expired form state entries from IndexedDB on app load @@ -9,6 +10,11 @@ cleanupExpiredStates().catch(() => { // Silent fail - cleanup is best-effort }); +// Initialize bfcache restoration handler +// This detects when Safari (and other browsers) restore pages from bfcache +// and refreshes auth session and project list to ensure state is current +initBfcacheHandler(); + function Root() { return ( diff --git a/packages/web/src/stores/projectStore.js b/packages/web/src/stores/projectStore.js index bf233c2c4..55f3b23ca 100644 --- a/packages/web/src/stores/projectStore.js +++ b/packages/web/src/stores/projectStore.js @@ -490,6 +490,49 @@ function createProjectStore() { return fetchProjectList(userId, { force: true }); } + /** + * Validate cached project list against current user ID + * Clears cache if user ID doesn't match + * @param {string} currentUserId - The current authenticated user's ID + */ + function validateProjectListCache(currentUserId) { + if (!currentUserId) { + // No user ID, clear the cache + setStore('projectList', { + items: [], + loaded: false, + loading: false, + error: null, + cachedUserId: null, + }); + // Clear localStorage cache + localStorage.removeItem(PROJECT_LIST_CACHE_KEY); + localStorage.removeItem(PROJECT_LIST_CACHE_TIMESTAMP_KEY); + localStorage.removeItem(PROJECT_LIST_CACHE_USER_ID_KEY); + return; + } + + const cachedUserId = store.projectList.cachedUserId; + + // If cached user ID doesn't match current user, clear the cache + if (cachedUserId && cachedUserId !== currentUserId) { + console.log( + '[projectStore] Cached project list belongs to different user, clearing cache', + ); + setStore('projectList', { + items: [], + loaded: false, + loading: false, + error: null, + cachedUserId: null, + }); + // Clear localStorage cache + localStorage.removeItem(PROJECT_LIST_CACHE_KEY); + localStorage.removeItem(PROJECT_LIST_CACHE_TIMESTAMP_KEY); + localStorage.removeItem(PROJECT_LIST_CACHE_USER_ID_KEY); + } + } + /** * Temporarily store pending project data during creation * This avoids passing non-serializable data through router state @@ -540,6 +583,7 @@ function createProjectStore() { // Actions - Project List (Dashboard) fetchProjectList, refreshProjectList, + validateProjectListCache, addProjectToList, updateProjectInList, removeProjectFromList, From bd87b2debfda05277603530d9eeedf93b2182e9f Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Fri, 26 Dec 2025 20:53:14 +0000 Subject: [PATCH 2/3] Apply Prettier formatting --- packages/web/src/stores/projectStore.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/web/src/stores/projectStore.js b/packages/web/src/stores/projectStore.js index 55f3b23ca..1ab883072 100644 --- a/packages/web/src/stores/projectStore.js +++ b/packages/web/src/stores/projectStore.js @@ -516,9 +516,7 @@ function createProjectStore() { // If cached user ID doesn't match current user, clear the cache if (cachedUserId && cachedUserId !== currentUserId) { - console.log( - '[projectStore] Cached project list belongs to different user, clearing cache', - ); + console.log('[projectStore] Cached project list belongs to different user, clearing cache'); setStore('projectList', { items: [], loaded: false, From ebff60cf3ec266ca127502333eb51193928c8fff Mon Sep 17 00:00:00 2001 From: Jacob Maynard Date: Fri, 26 Dec 2025 15:20:12 -0600 Subject: [PATCH 3/3] fix function reuse --- packages/web/src/stores/projectStore.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/web/src/stores/projectStore.js b/packages/web/src/stores/projectStore.js index 1ab883072..af84f83e1 100644 --- a/packages/web/src/stores/projectStore.js +++ b/packages/web/src/stores/projectStore.js @@ -506,9 +506,7 @@ function createProjectStore() { cachedUserId: null, }); // Clear localStorage cache - localStorage.removeItem(PROJECT_LIST_CACHE_KEY); - localStorage.removeItem(PROJECT_LIST_CACHE_TIMESTAMP_KEY); - localStorage.removeItem(PROJECT_LIST_CACHE_USER_ID_KEY); + saveCachedProjectList(null, null); return; } @@ -525,9 +523,7 @@ function createProjectStore() { cachedUserId: null, }); // Clear localStorage cache - localStorage.removeItem(PROJECT_LIST_CACHE_KEY); - localStorage.removeItem(PROJECT_LIST_CACHE_TIMESTAMP_KEY); - localStorage.removeItem(PROJECT_LIST_CACHE_USER_ID_KEY); + saveCachedProjectList(null, null); } }