-
-
Save lordsarcastic/ce608a3df278e036b39dd456d47402a7 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import axios, { AxiosInstance, AxiosResponse, AxiosError } from 'axios' | |
| // ----------------------------------------------------------------------------- | |
| // Constants | |
| // ----------------------------------------------------------------------------- | |
| const TOKEN_EXPIRY = { | |
| ACCESS_TOKEN: 1800, | |
| REFRESH_TOKEN: 2592000, | |
| } as const | |
| const CACHE_KEYS = { | |
| ACCESS_TOKEN: 'app:access_token', | |
| REFRESH_TOKEN: 'app:refresh_token', | |
| } as const | |
| // ----------------------------------------------------------------------------- | |
| // Simple In-Memory Cache (swap out for Redis/localStorage if needed) | |
| // ----------------------------------------------------------------------------- | |
| const memoryCache = new Map<string, { value: string; expires: number }>() | |
| const getFromMemory = (key: string) => { | |
| const entry = memoryCache.get(key) | |
| if (!entry) return null | |
| if (Date.now() > entry.expires) { | |
| memoryCache.delete(key) | |
| return null | |
| } | |
| return entry.value | |
| } | |
| const setInMemory = (key: string, value: string, ttlSeconds: number) => { | |
| memoryCache.set(key, { | |
| value, | |
| expires: Date.now() + ttlSeconds * 1000, | |
| }) | |
| } | |
| const delFromMemory = (key: string) => memoryCache.delete(key) | |
| // ----------------------------------------------------------------------------- | |
| // Configuration | |
| // ----------------------------------------------------------------------------- | |
| const CONFIG = { | |
| BASE_URL: process.env.API_BASE_URL || 'https://api.example.com', | |
| } | |
| // ----------------------------------------------------------------------------- | |
| // API Instance | |
| // ----------------------------------------------------------------------------- | |
| const api: AxiosInstance = axios.create({ | |
| baseURL: CONFIG.BASE_URL, | |
| timeout: 30000, | |
| timeoutErrorMessage: 'Request timed out', | |
| }) | |
| // ----------------------------------------------------------------------------- | |
| // Token Management | |
| // ----------------------------------------------------------------------------- | |
| const tokenRefreshLock = new Map<string, Promise<string | null>>() | |
| const storeTokens = async (accessToken: string, refreshToken?: string) => { | |
| setInMemory(CACHE_KEYS.ACCESS_TOKEN, accessToken, TOKEN_EXPIRY.ACCESS_TOKEN) | |
| if (refreshToken) { | |
| setInMemory(CACHE_KEYS.REFRESH_TOKEN, refreshToken, TOKEN_EXPIRY.REFRESH_TOKEN) | |
| } | |
| } | |
| const clearTokens = async () => { | |
| delFromMemory(CACHE_KEYS.ACCESS_TOKEN) | |
| delFromMemory(CACHE_KEYS.REFRESH_TOKEN) | |
| } | |
| /** | |
| * Returns a valid access token or undefined if we can't get one. | |
| * Automatically refreshes if expired, with a concurrency lock. | |
| */ | |
| export const getAccessToken = async (forceRenew = false): Promise<string | null> => { | |
| const lockKey = 'access_token_refresh' | |
| const cachedFn = tokenRefreshLock.get(lockKey) ?? null; | |
| if (cachedFn) return cachedFn | |
| const refreshPromise = (async () => { | |
| const cachedAccess = getFromMemory(CACHE_KEYS.ACCESS_TOKEN) | |
| if (cachedAccess && !forceRenew) return cachedAccess | |
| const refreshToken = getFromMemory(CACHE_KEYS.REFRESH_TOKEN) | |
| if (!refreshToken) { | |
| console.error('Token generation error: No refresh token found') | |
| return null; | |
| } | |
| // Replace this with your real refresh endpoint | |
| const response = await api.post('/auth/token', { refresh_token: refreshToken }) | |
| const { access_token, refresh_token: newRefreshToken } = response.data || {} | |
| if (!access_token) { | |
| console.error('Token generation error: No access token found') | |
| return null; | |
| } | |
| if (!response.ok) { | |
| console.error('Token generation error: Failed to refresh token', response.statusText) | |
| return null; | |
| } | |
| await storeTokens(access_token, newRefreshToken) | |
| tokenRefreshLock.delete(lockKey) | |
| return access_token | |
| })() | |
| tokenRefreshLock.set(lockKey, refreshPromise) | |
| return refreshPromise | |
| } | |
| const makeRequest = async <T>( | |
| endpoint: string, | |
| method: 'GET' | 'POST' | 'PUT' | 'DELETE', | |
| data: Record<string, unknown> | FormData | undefined, | |
| token: string, | |
| ): Promise<AxiosResponse<T>> => { | |
| const response: AxiosResponse<T> = await api.request({ | |
| url: endpoint, | |
| method, | |
| ...(data && { data }), | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| Authorization: `Bearer ${token}`, | |
| }, | |
| signal: AbortSignal.timeout(5000), | |
| }) | |
| } | |
| // ----------------------------------------------------------------------------- | |
| // Generic API Caller with Retry + Exponential Backoff | |
| // ----------------------------------------------------------------------------- | |
| const ACCESS_TOKEN_RETRIEVAL_ERROR = 'Unable to obtain access token' | |
| const SERVER_ERROR = 'Server error' | |
| enum Result { | |
| Ok, | |
| Err | |
| } | |
| type Failable<T, E> = { | |
| result: Result, | |
| body?: T | E, | |
| }; | |
| export const callApi = async <T>( | |
| endpoint: string, | |
| method: 'GET' | 'POST' | 'PUT' | 'DELETE', | |
| data?: Record<string, unknown> | FormData, | |
| options: { maxRetries?: number; forceRenew?: boolean } = {} | |
| ): Promise<Failable<T, AxiosError>> => { | |
| const { maxRetries = 3, forceRenew = false } = options | |
| const getToken = async (): Promise<Failable<string, AxiosError>> => { | |
| let token = await getAccessToken(forceRenew); | |
| if (!token) return { | |
| result: Result.Err, | |
| body: new Error(ACCESS_TOKEN_RETRIEVAL_ERROR) as AxiosError | |
| } | |
| return { | |
| result: Result.Ok, | |
| body: token | |
| }; | |
| } | |
| let tokenCall = await getToken(); | |
| let token: string; | |
| if (tokenCall.result === Result.Err) { | |
| return { | |
| result: Result.Err, | |
| body: tokenCall.body | |
| } | |
| } else { | |
| token = tokenCall.body as string; | |
| } | |
| let response: AxiosResponse<T>; | |
| let retryCount = maxRetries; | |
| const cancelTokenSource = axios.CancelToken.source() | |
| while (retryCount > 0) { | |
| response = await makeRequest<T>(endpoint, method, data, token); | |
| const message = response?.data?.message || 'Unknown error' | |
| if (response.status >= 200 && response.status < 300) { | |
| return response.data; | |
| } else if (response.status === 401) { | |
| let tokenCall = await getToken(); | |
| if (tokenCall.result === Result.Err) { | |
| return { | |
| result: Result.Err, | |
| body: tokenCall.body | |
| } | |
| } else { | |
| token = tokenCall.body as string; | |
| } | |
| } else if (response.status > 401 && response.status < 500) { | |
| return { | |
| result: Result.Err, | |
| body: new Error(message) as AxiosError | |
| } | |
| } | |
| console.warn(`API call failed: ${message} — retrying (${retryCount - 1} retries left)`) | |
| retryCount--; | |
| await new Promise((r) => setTimeout(r, Math.pow(2, 3 - retryCount) * 1000)) | |
| } | |
| return { | |
| result: Result.Err, | |
| body: new Error(SERVER_ERROR) as AxiosError | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment