mirror of
https://github.com/vernu/textbee.git
synced 2026-07-31 01:17:52 -04:00
- prefetch route-tab links so tab clicks reuse a cached payload instead of paying an uncached server round trip each time - add loading.tsx boundaries (dashboard root and each section) so clicks paint within a frame; section-level files keep the header and tabs mounted while only the content area swaps - link the sidebar Account item and the checkout page straight to /dashboard/account/billing. The /dashboard/account redirect stub stays for old links, but no internal link pays the extra hop anymore. The stub also dropped query params, which silently ate the plan-change success toast. - set QueryClient defaults (staleTime 60s, no focus refetch, retry 1); mutations already invalidate their keys, so the user's own changes stay instant. Device messages and webhook deliveries get a 15s staleTime since they change from outside the tab. - replace the axios getCachedSession TTL cache with a token seeded from the server session in Providers and kept in sync by a session bridge. Requests attach the token synchronously; /api/auth/session is only a deduped fallback, instead of a refetch every 2 minutes with a thundering herd on expiry. - swap the billing card's 16px loading spinner for a card-shaped skeleton so the tab no longer looks blank while loading Tests: interceptor seeding/dedupe/signed-out behavior, provider defaults and token seeding, nav active-state matching, tab prefetch, billing loading state, plus e2e coverage for direct-to-billing navigation and an at-most-one-session-call budget guard. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
68 lines
2.1 KiB
TypeScript
68 lines
2.1 KiB
TypeScript
import axios from 'axios'
|
|
import { getSession } from 'next-auth/react'
|
|
|
|
const httpBrowserClient = axios.create({
|
|
baseURL: process.env.NEXT_PUBLIC_API_BASE_URL || '',
|
|
})
|
|
|
|
// API access token, seeded from the server-fetched session by Providers and
|
|
// kept current by its SessionTokenBridge. Held in module state so the request
|
|
// interceptor attaches it synchronously instead of paying a /api/auth/session
|
|
// round trip per request (Vercel cost and added latency). undefined means not
|
|
// seeded yet; null means known signed out, so auth pages never fetch either.
|
|
let sessionToken: string | null | undefined
|
|
|
|
export function setSessionToken(token: string | null) {
|
|
sessionToken = token
|
|
}
|
|
|
|
// Fallback for the rare request that fires before the token is seeded (deep
|
|
// hard load). One shared promise so a burst of queries costs one session call.
|
|
let sessionFetch: Promise<string | null> | null = null
|
|
|
|
function fetchSessionToken() {
|
|
if (!sessionFetch) {
|
|
sessionFetch = getSession()
|
|
.then((session) => {
|
|
sessionToken = session?.user?.accessToken ?? null
|
|
return sessionToken
|
|
})
|
|
.finally(() => {
|
|
sessionFetch = null
|
|
})
|
|
}
|
|
return sessionFetch
|
|
}
|
|
|
|
httpBrowserClient.interceptors.request.use(async (config) => {
|
|
const token =
|
|
sessionToken === undefined ? await fetchSessionToken() : sessionToken
|
|
|
|
if (token) {
|
|
config.headers.Authorization = `Bearer ${token}`
|
|
}
|
|
return config
|
|
})
|
|
|
|
// Global session-expiry handling: any 401 from the API means the stored token
|
|
// is no longer valid, so send the user to logout. This replaces the previous
|
|
// per-navigation whoAmI check in the layout wrapper.
|
|
httpBrowserClient.interceptors.response.use(
|
|
(response) => response,
|
|
(error) => {
|
|
if (
|
|
typeof window !== 'undefined' &&
|
|
error?.response?.status === 401
|
|
) {
|
|
const { pathname } = window.location
|
|
if (!pathname.includes('/logout') && !pathname.includes('/login')) {
|
|
setSessionToken(null)
|
|
window.location.href = '/logout'
|
|
}
|
|
}
|
|
return Promise.reject(error)
|
|
}
|
|
)
|
|
|
|
export default httpBrowserClient
|