@accounter/client
Version:
86 lines (78 loc) • 2.09 kB
text/typescript
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;
}