Token Refresh Manager
TokenRefreshManager is a static utility class that handles transparent access-token refresh when a 401 or UNAUTHENTICATED response is detected. It provides mutex deduplication so that concurrent callers share a single in-flight refresh, preventing token-rotation race conditions.
API

| Method | Signature | Description |
|---|---|---|
refresh |
() → Promise<UserRefreshTokenResponseType> |
Reads current cookies, calls IdentityService.refreshToken(), writes new tokens, returns result. |
reset |
() → void |
Clears the in-flight promise. Exposed for test isolation only. |
How It Works

- The first call creates an in-flight promise and stores it.
- Any concurrent call while the promise is pending receives the same promise — no duplicate API calls.
- On resolution (success or failure), the in-flight promise is cleared so subsequent calls start fresh.
Integration Points
TokenRefreshManager.refresh() is called automatically by both transport layers when they detect a 401:
| Transport | Detection | Behaviour |
|---|---|---|
Fetch.json |
HTTP res.status === 401 |
Refresh → retry the request once with fresh auth headers |
GraphQLClient |
networkError.statusCode === 401 or graphQLErrors[].extensions.code === 'UNAUTHENTICATED' |
Refresh → retry the query/mutation once |
Both transports enforce a retry-once guard (_isRetry flag) to prevent infinite loops if the refreshed token is also rejected.
Error Handling
| Scenario | Behaviour |
|---|---|
Missing site or vl-refreshToken cookie |
Throws AppError('auth.token_refresh_failed') immediately — no API call made. |
IdentityService.refreshToken rejects |
Error propagates to all waiting callers. In-flight promise is cleared. |
| Retry itself returns 401 | The transport throws the original error — no second refresh attempt. |
Cookie TTLs
After a successful refresh, cookies are updated with their canonical TTLs from CookieTTL:
| Cookie | TTL |
|---|---|
vl-accessToken |
CookieTTL.identity.accessToken — 12 hours |
vl-refreshToken |
CookieTTL.identity.refreshToken — 7 days |
Design Decisions
- Singleton, not hook. The manager is a static class because it must be accessible from both
Fetch(a static class) andGraphQLClient(also static). A React hook would not be reachable from these non-React call sites. - No automatic logout. If refresh fails, the error propagates to the caller. The caller's own error handling (typically the
AsyncHandler→ hook → toast chain) is responsible for surfacing the failure. A forced redirect to/on refresh failure could interrupt in-progress user work. - Promise-lock, not queue. A simple shared-promise pattern is sufficient because there are only two token slots to rotate. A full queue would add complexity without benefit.
Testing
Test file: src/__tests__/lib/internal/TokenRefreshManager.test.ts
The test suite mocks IdentityService and CookieManager and validates:
- Happy path — correct arguments and cookie writes
- Mutex deduplication — three concurrent calls produce one service call
- Sequential calls — fresh call after completion triggers a new refresh
- Missing cookies — throws before calling the service
- Error propagation — all concurrent waiters receive the same rejection
- Recovery — a new call after a failed refresh retries successfully