From 428390dfa26349130ac75bc6d708449f8f29a349 Mon Sep 17 00:00:00 2001 From: Ray Date: Thu, 7 May 2026 19:07:36 -0400 Subject: [PATCH] fix(react): wrap fetchAccessToken in new Promise to fix useConvexAuth on Hermes V1 The /convex/token response triggers a session rotation (via Better Auth's Set-Cookie processing) plus a setCachedToken call inside the bridge's .then. The next render rebuilds fetchAccessToken's useCallback (keyed on [sessionId]) and fires ConvexAuthStateFirstEffect's client.setAuth a second time. On Hermes V1 native async (Expo SDK 56 canary 2026-05-05+ since expo/expo#45345 dropped @babel/plugin-transform-async-to-generator), that second setAuth lands inside the first setConfig's await window in authentication_manager.ts. fetchTokenAndGuardAgainstRace bumps configVersion on entry and the original await sees the stale value, returning isFromOutdatedConfig: true. setConfig bails without resumeSocket() and the chain repeats. Drop the async keyword and wrap the body in new Promise(executor) directly. The constructor's resolve(thenable) schedules a NewPromiseResolveThenableJob microtask, the same hop regenerator's _asyncToGenerator provides. With the hop in place the second setAuth lands after the first setConfig finishes rather than during its await window. --- src/react/index.tsx | 48 ++++++++++++++++++++++++--------------------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/src/react/index.tsx b/src/react/index.tsx index dc10909a..2f906878 100644 --- a/src/react/index.tsx +++ b/src/react/index.tsx @@ -127,30 +127,34 @@ function useUseAuthFromBetterAuth( } }, [session, isSessionPending]); const fetchAccessToken = useCallback( - async ({ + ({ forceRefreshToken = false, }: { forceRefreshToken?: boolean } = {}) => { - if (cachedToken && !forceRefreshToken) { - return cachedToken; - } - if (!forceRefreshToken && pendingTokenRef.current) { - return pendingTokenRef.current; - } - pendingTokenRef.current = authClient.convex - .token({ fetchOptions: { throw: false } }) - .then(({ data }) => { - const token = data?.token || null; - setCachedToken(token); - return token; - }) - .catch(() => { - setCachedToken(null); - return null; - }) - .finally(() => { - pendingTokenRef.current = null; - }); - return pendingTokenRef.current; + return new Promise((resolve, reject) => { + if (cachedToken && !forceRefreshToken) { + resolve(cachedToken); + return; + } + if (!forceRefreshToken && pendingTokenRef.current) { + pendingTokenRef.current.then(resolve, reject); + return; + } + pendingTokenRef.current = authClient.convex + .token({ fetchOptions: { throw: false } }) + .then(({ data }) => { + const token = data?.token || null; + setCachedToken(token); + return token; + }) + .catch(() => { + setCachedToken(null); + return null; + }) + .finally(() => { + pendingTokenRef.current = null; + }); + pendingTokenRef.current.then(resolve, reject); + }); }, // Build a new fetchAccessToken to trigger setAuth() whenever the // session changes.