diff --git a/packages/shared/src/sessions.ts b/packages/shared/src/sessions.ts index e89e08776..788fc1756 100644 --- a/packages/shared/src/sessions.ts +++ b/packages/shared/src/sessions.ts @@ -124,6 +124,14 @@ export class InternalSession { if (ofTokens.refreshToken) { return `refresh-${ofTokens.refreshToken}`; } else if (ofTokens.accessToken) { + // Access-only sessions (no refresh token) are keyed by the underlying session's `refresh_token_id`, not the + // access token string: access tokens get re-minted frequently, and keying by the raw token would spawn a new + // session (and cold-invalidate every session-scoped cache) on each refresh. Falls back to the raw token if + // the JWT can't be decoded. + const refreshTokenId = decodeAccessTokenIfValid(ofTokens.accessToken)?.refresh_token_id; + if (refreshTokenId) { + return `access-session-${refreshTokenId}`; + } return `access-${ofTokens.accessToken}`; } else { return "not-logged-in"; @@ -210,6 +218,21 @@ export class InternalSession { return accessToken ? { accessToken, refreshToken: this._refreshToken } : null; } + /** + * Installs a fresh access token into this session in place, keeping the session object (and therefore every + * session-scoped cache) stable instead of constructing a new InternalSession. Caller must pass a token that + * belongs to the same session. No-op if the session is invalid, the token can't be decoded, or it's unchanged; + * never clears an existing token. + */ + updateAccessToken(accessToken: string | null) { + if (this._knownToBeInvalid.get()) return; + if (!accessToken) return; + const newAccessToken = AccessToken.createIfValid(accessToken); + if (!newAccessToken) return; + if (this._accessToken.get()?.token === newAccessToken.token) return; + this._accessToken.set(newAccessToken); + } + /** * Manually mark the access token as expired, even if the date on its payload may still be valid. * diff --git a/packages/template/src/lib/hexclave-app/apps/implementations/client-app-impl.ts b/packages/template/src/lib/hexclave-app/apps/implementations/client-app-impl.ts index 49fd65643..90e7011db 100644 --- a/packages/template/src/lib/hexclave-app/apps/implementations/client-app-impl.ts +++ b/packages/template/src/lib/hexclave-app/apps/implementations/client-app-impl.ts @@ -1547,10 +1547,14 @@ export class _HexclaveClientAppImplIncomplete {}); + // If these tokens resolve to a session we already have (eg. the RDE dashboard re-installing a freshly minted + // access token for the same access-only session), push the new token into it in place; constructing a new + // session here would cold-invalidate every session-scoped cache and suspend the UI on each refresh. + const session = this._getSessionFromTokenStore(tokenStore); + session.updateAccessToken(tokens.accessToken); + + // Pre-fetch the current user so the cache is warm when useUser() re-renders (write-only, so it never suspends). + this._currentUserCache.getOrWait([session], "write-only").catch(() => {}); } protected _getTokenStoreInitForFreshTokens(tokens: { accessToken: string | null, refreshToken: string }): TokenStoreInit | undefined {