diff --git a/src/app/routes/__root.tsx b/src/app/routes/__root.tsx
index e630790..03b49f4 100644
--- a/src/app/routes/__root.tsx
+++ b/src/app/routes/__root.tsx
@@ -11,6 +11,7 @@ import { type QueryClient } from "@tanstack/react-query";
import { ensureSuperTokensFrontend } from "@/lib/supertokens/client";
import { AuthContextType } from "@/contexts/auth-context";
import Providers from "@/features/core/components/providers";
+import { SessionMonitor } from "@/components/session-monitor";
import { ColorSchemeScript, mantineHtmlProps } from "@mantine/core";
import { HeaderConfig } from "@/features/core/types/header-config";
import { playerQueries } from "@/features/players/queries";
@@ -126,6 +127,7 @@ function RootComponent() {
return (
+
diff --git a/src/app/routes/refresh-session.tsx b/src/app/routes/refresh-session.tsx
index dad66b0..9d627ed 100644
--- a/src/app/routes/refresh-session.tsx
+++ b/src/app/routes/refresh-session.tsx
@@ -2,7 +2,8 @@ import { createFileRoute } from '@tanstack/react-router'
import { useEffect, useRef } from 'react'
import FullScreenLoader from '@/components/full-screen-loader'
import { attemptRefreshingSession } from 'supertokens-web-js/recipe/session'
-import { resetRefreshFlag } from '@/lib/supertokens/client'
+import { resetRefreshFlag, getOrCreateRefreshPromise } from '@/lib/supertokens/client'
+import { logger } from '@/lib/supertokens'
export const Route = createFileRoute('/refresh-session')({
component: RouteComponent,
@@ -17,23 +18,31 @@ function RouteComponent() {
const handleRefresh = async () => {
try {
- resetRefreshFlag();
- const refreshed = await attemptRefreshingSession()
+ logger.info("Refresh session route: starting refresh");
+
+ const refreshed = await getOrCreateRefreshPromise(async () => {
+ return await attemptRefreshingSession();
+ });
if (refreshed) {
- const urlParams = new URLSearchParams(window.location.search)
- const redirect = urlParams.get('redirect')
+ logger.info("Refresh session route: refresh successful");
+ const urlParams = new URLSearchParams(window.location.search);
+ const redirect = urlParams.get('redirect');
if (redirect && !redirect.includes('_serverFn') && !redirect.includes('/api/')) {
- window.location.href = decodeURIComponent(redirect)
+ logger.info("Refresh session route: redirecting to", redirect);
+ window.location.href = decodeURIComponent(redirect);
} else {
+ logger.info("Refresh session route: redirecting to home");
window.location.href = '/';
}
} else {
- window.location.href = '/login'
+ logger.warn("Refresh session route: refresh failed, redirecting to login");
+ window.location.href = '/login';
}
} catch (error) {
- window.location.href = '/login'
+ logger.error("Refresh session route: error during refresh", error);
+ window.location.href = '/login';
}
}
diff --git a/src/components/session-monitor.tsx b/src/components/session-monitor.tsx
new file mode 100644
index 0000000..ee4e3ec
--- /dev/null
+++ b/src/components/session-monitor.tsx
@@ -0,0 +1,60 @@
+import { useEffect, useRef } from 'react';
+import { doesSessionExist } from 'supertokens-web-js/recipe/session';
+import { getOrCreateRefreshPromise } from '@/lib/supertokens/client';
+import { attemptRefreshingSession } from 'supertokens-web-js/recipe/session';
+import { logger } from '@/lib/supertokens';
+
+export function SessionMonitor() {
+ const lastRefreshTimeRef = useRef(0);
+ const REFRESH_COOLDOWN = 30 * 1000;
+
+ useEffect(() => {
+ if (typeof window === 'undefined') return;
+
+ const handleVisibilityChange = async () => {
+ if (document.visibilityState !== 'visible') return;
+
+ const publicRoutes = ['/login', '/logout', '/refresh-session'];
+ if (publicRoutes.some(route => window.location.pathname === route)) {
+ return;
+ }
+
+ const now = Date.now();
+ if (now - lastRefreshTimeRef.current < REFRESH_COOLDOWN) {
+ logger.info('Session monitor: skipping refresh (cooldown)');
+ return;
+ }
+
+ try {
+ const sessionExists = await doesSessionExist();
+ if (!sessionExists) {
+ logger.info('Session monitor: no session exists, skipping refresh');
+ return;
+ }
+
+ logger.info('Session monitor: tab became visible, refreshing session');
+
+ const refreshed = await getOrCreateRefreshPromise(async () => {
+ return await attemptRefreshingSession();
+ });
+
+ if (refreshed) {
+ lastRefreshTimeRef.current = Date.now();
+ logger.info('Session monitor: session refreshed successfully');
+ } else {
+ logger.warn('Session monitor: refresh returned false');
+ }
+ } catch (error) {
+ logger.error('Session monitor: error refreshing session', error);
+ }
+ };
+
+ document.addEventListener('visibilitychange', handleVisibilityChange);
+
+ return () => {
+ document.removeEventListener('visibilitychange', handleVisibilityChange);
+ };
+ }, []);
+
+ return null;
+}
diff --git a/src/features/players/queries.ts b/src/features/players/queries.ts
index eb61288..53f2b49 100644
--- a/src/features/players/queries.ts
+++ b/src/features/players/queries.ts
@@ -1,5 +1,8 @@
import { useServerSuspenseQuery } from "@/lib/tanstack-query/hooks";
import { listPlayers, getPlayer, getUnassociatedPlayers, fetchMe, getPlayerStats, getAllPlayerStats, getPlayerMatches, getUnenrolledPlayers, getPlayersActivity } from "./server";
+import { logger } from '@/lib/supertokens';
+
+let queryRefreshRedirect: Promise | null = null;
export const playerKeys = {
auth: ['auth'],
@@ -54,24 +57,45 @@ export const playerQueries = {
export const useMe = () => {
const { queryKey, queryFn } = playerQueries.auth();
- return useServerSuspenseQuery({
- queryKey,
+ return useServerSuspenseQuery({
+ queryKey,
queryFn,
options: {
- staleTime: 0,
- refetchOnMount: true,
+ staleTime: 30 * 1000,
+ refetchOnMount: false,
+ refetchOnWindowFocus: true,
retry: (failureCount, error: any) => {
if (error?.response?.status === 401) {
const errorData = error?.response?.data;
- if (errorData?.error === "SESSION_REFRESH_REQUIRED") {
- const currentUrl = window.location.pathname + window.location.search;
- window.location.href = `/refresh-session?redirect=${encodeURIComponent(currentUrl)}`;
+ if (errorData?.error === 'SESSION_REFRESH_REQUIRED') {
+ logger.warn("Query detected SESSION_REFRESH_REQUIRED");
+
+ if (!queryRefreshRedirect) {
+ const currentUrl = window.location.pathname + window.location.search;
+ logger.info("Query initiating refresh redirect to:", currentUrl);
+
+ queryRefreshRedirect = new Promise((resolve) => {
+ setTimeout(() => {
+ window.location.href = `/refresh-session?redirect=${encodeURIComponent(currentUrl)}`;
+ resolve();
+ }, 100);
+ });
+
+ queryRefreshRedirect.finally(() => {
+ setTimeout(() => {
+ queryRefreshRedirect = null;
+ }, 1000);
+ });
+ } else {
+ logger.info("Query: refresh redirect already in progress");
+ }
+
return false;
}
}
return failureCount < 3;
- }
- }
+ },
+ },
});
};
diff --git a/src/lib/supertokens/client.ts b/src/lib/supertokens/client.ts
index fbed58a..d994557 100644
--- a/src/lib/supertokens/client.ts
+++ b/src/lib/supertokens/client.ts
@@ -4,30 +4,34 @@ import Passwordless from "supertokens-web-js/recipe/passwordless";
import { appInfo } from "./config";
import { logger } from "./";
-let refreshAttemptCount = 0;
+let refreshPromise: Promise | null = null;
export const resetRefreshFlag = () => {
- refreshAttemptCount = 0;
+ refreshPromise = null;
};
-const setupFetchInterceptor = () => {
- if (typeof window === 'undefined') return;
+export const getOrCreateRefreshPromise = (refreshFn: () => Promise): Promise => {
+ if (refreshPromise) {
+ logger.info("Reusing existing refresh promise");
+ return refreshPromise;
+ }
- const originalFetch = window.fetch;
- //@ts-ignore
- window.fetch = async (resource: RequestInfo | URL, options?: RequestInit) => {
- const url = typeof resource === 'string' ? resource :
- resource instanceof URL ? resource.toString() : resource.url;
+ logger.info("Creating new refresh promise");
+ refreshPromise = refreshFn()
+ .then((result) => {
+ logger.info("Refresh completed successfully:", result);
+ setTimeout(() => {
+ refreshPromise = null;
+ }, 500);
+ return result;
+ })
+ .catch((error) => {
+ logger.error("Refresh failed:", error);
+ refreshPromise = null;
+ throw error;
+ });
- if (url.includes('/api/auth/session/refresh')) {
- refreshAttemptCount++;
- if (refreshAttemptCount > 1) {
- throw new Error('Duplicate refresh attempt blocked');
- }
- }
-
- return originalFetch.call(window, resource, options);
- };
+ return refreshPromise;
};
export const frontendConfig = () => {
@@ -53,7 +57,6 @@ export function ensureSuperTokensFrontend() {
if (typeof window === "undefined") return;
if (!initialized) {
- setupFetchInterceptor();
SuperTokens.init(frontendConfig());
initialized = true;
logger.info("SuperTokens initialized");
diff --git a/src/lib/tanstack-query/hooks/use-server-mutation.ts b/src/lib/tanstack-query/hooks/use-server-mutation.ts
index 673768c..9484785 100644
--- a/src/lib/tanstack-query/hooks/use-server-mutation.ts
+++ b/src/lib/tanstack-query/hooks/use-server-mutation.ts
@@ -1,8 +1,10 @@
import { useMutation, UseMutationOptions } from "@tanstack/react-query";
import { ServerResult } from "../types";
import toast from '@/lib/sonner'
+import { logger } from '@/lib/supertokens'
-let isMutationRefreshingSession = false;
+
+let sessionRefreshRedirect: Promise | null = null;
export function useServerMutation(
options: Omit, 'mutationFn'> & {
@@ -39,24 +41,39 @@ export function useServerMutation(
} catch (error: any) {
if (error?.response?.status === 401) {
try {
- const errorData = typeof error.response.data === 'string'
- ? JSON.parse(error.response.data)
+ const errorData = typeof error.response.data === 'string'
+ ? JSON.parse(error.response.data)
: error.response.data;
-
+
if (errorData?.error === "SESSION_REFRESH_REQUIRED") {
- if (!isMutationRefreshingSession) {
- isMutationRefreshingSession = true;
+ logger.warn("Mutation detected SESSION_REFRESH_REQUIRED");
+
+ if (!sessionRefreshRedirect) {
const currentUrl = window.location.pathname + window.location.search;
- setTimeout(() => {
- isMutationRefreshingSession = false;
- }, 1000);
- window.location.href = `/refresh-session?redirect=${encodeURIComponent(currentUrl)}`;
+ logger.info("Mutation initiating refresh redirect to:", currentUrl);
+
+ sessionRefreshRedirect = new Promise((resolve) => {
+ setTimeout(() => {
+ window.location.href = `/refresh-session?redirect=${encodeURIComponent(currentUrl)}`;
+ resolve();
+ }, 100);
+ });
+
+ sessionRefreshRedirect.finally(() => {
+ setTimeout(() => {
+ sessionRefreshRedirect = null;
+ }, 1000);
+ });
+ } else {
+ logger.info("Mutation: refresh redirect already in progress, waiting...");
+ await sessionRefreshRedirect;
}
+
throw new Error("SESSION_REFRESH_REQUIRED");
}
} catch (parseError) {}
}
-
+
throw error;
}
},