import type { InMemoryCacheConfig } from '@apollo/client'; import { ApolloClient, from, split, ApolloLink, HttpLink, InMemoryCache, } from '@apollo/client'; import { GraphQLWsLink } from '@apollo/client/link/subscriptions'; import { getMainDefinition } from '@apollo/client/utilities'; import { createClient as createWSClient } from 'graphql-ws'; import { onError } from '@apollo/client/link/error'; import { RetryLink } from '@apollo/client/link/retry'; import ApolloLinkTimeout from 'apollo-link-timeout'; import { localLoggerFactory } from '@vegaprotocol/utils'; import { useHeaderStore } from './header-store'; const isBrowser = typeof window !== 'undefined'; const NOT_FOUND = 'NotFound'; export type ClientOptions = { url?: string; cacheConfig?: InMemoryCacheConfig; retry?: boolean; connectToDevTools?: boolean; connectToHeaderStore?: boolean; }; export function createClient({ url, cacheConfig, retry = true, connectToDevTools = true, connectToHeaderStore = true, }: ClientOptions) { if (!url) { throw new Error('url must be passed into createClient!'); } const urlHTTP = new URL(url); const urlWS = new URL(url); // Replace http with ws, preserving if its a secure connection eg. https => wss urlWS.protocol = urlWS.protocol.replace('http', 'ws'); const noOpLink = new ApolloLink((operation, forward) => { return forward(operation); }); const timeoutLink = new ApolloLinkTimeout(10000); const enlargedTimeoutLink = new ApolloLinkTimeout(100000); const headerLink = connectToHeaderStore ? new ApolloLink((operation, forward) => { return forward(operation).map((response) => { const context = operation.getContext(); const r = context['response']; const blockHeight = r?.headers.get('x-block-height'); const timestamp = r?.headers.get('x-block-timestamp'); if (blockHeight && timestamp) { const state = useHeaderStore.getState(); const urlState = state[r.url]; if ( !urlState?.blockHeight || urlState.blockHeight !== blockHeight ) { useHeaderStore.setState({ ...state, [r.url]: { blockHeight: Number(blockHeight), timestamp: new Date(Number(timestamp.slice(0, -6))), }, }); } } return response; }); }) : noOpLink; const retryLink = retry ? new RetryLink({ delay: { initial: 300, max: 10000, jitter: true, }, }) : noOpLink; const httpLink = new HttpLink({ uri: urlHTTP.href, credentials: 'same-origin', }); const wsLink = isBrowser ? new GraphQLWsLink( createWSClient({ url: urlWS.href, }) ) : noOpLink; const splitLink = isBrowser ? split( ({ query }) => { const definition = getMainDefinition(query); return ( definition.kind === 'OperationDefinition' && definition.operation === 'subscription' ); }, wsLink, httpLink ) : httpLink; const errorLink = onError(({ graphQLErrors, networkError }) => { if (graphQLErrors) { graphQLErrors.forEach((e) => { if (e.extensions && e.extensions['type'] !== NOT_FOUND) { localLoggerFactory({ application: 'apollo-client' }).error(e); } }); } if (networkError) { localLoggerFactory({ application: 'apollo-client' }).error(networkError); } }); const composedTimeoutLink = split( ({ getContext }) => Boolean(getContext()['isEnlargedTimeout']), enlargedTimeoutLink, timeoutLink ); return new ApolloClient({ link: from([ errorLink, composedTimeoutLink, retryLink, headerLink, splitLink, ]), cache: new InMemoryCache(cacheConfig), connectToDevTools, }); }