UNPKG

@accounter/client

Version:

86 lines (78 loc) 2.09 kB
import { createClient, fetchExchange, mapExchange, type AnyVariables, type Client, type Operation, } from 'urql'; import { authExchange } from '@urql/exchange-auth'; import { UserService } from '../services/user-service.js'; import { handleUrqlError } from './urql-error-handler.js'; /** * Singleton URQL client for use in loaders and server-side operations * This is separate from the Provider client to avoid React context dependencies */ let globalClient: Client | null = null; export function getUrqlClient(): Client { if (globalClient) { return globalClient; } const authService = new UserService(); let url: string; switch (import.meta.env.MODE) { case 'production': { url = 'https://accounter.onrender.com/graphql'; break; } case 'staging': { url = 'https://accounter-staging.onrender.com/graphql'; break; } default: { url = 'http://localhost:4000/graphql'; break; } } globalClient = createClient({ url, exchanges: [ mapExchange({ onResult(result) { handleUrqlError(result); }, }), authExchange(async utils => { return { addAuthToOperation(operation): Operation<void, AnyVariables> { const token = authService.authToken(); if (!token) { return operation; } return utils.appendHeaders(operation, { Authorization: token, }); }, didAuthError(error, _operation): boolean { return ( error?.response?.status === 401 || error?.graphQLErrors?.some(e => e.extensions?.code === 'FORBIDDEN') ); }, async refreshAuth(): Promise<void> { authService.logout(); // Redirect handled by route loader }, }; }), fetchExchange, ], }); return globalClient; } /** * Reset the global client (useful for tests or logout) */ export function resetUrqlClient(): void { globalClient = null; }