import { ApolloLink, Operation, FetchResult, Observable } from '@apollo/client/core'; import { print, GraphQLError } from 'graphql'; import { createClient, stringifyMessage, Client, MessageType } from 'graphql-ws'; import { getAccessToken } from 'utils/auth'; export class WebSocketLink extends ApolloLink { private activeSocket?: WebSocket; private client: Client; private restartTimeout?: NodeJS.Timeout; constructor(url: string) { super(); this.client = createClient({ disablePong: true, url, connectionParams: async () => ({ Authorization: await getAccessToken(), }), on: { opened: (socket) => this.onOpen(socket as WebSocket), ping: () => { this.sendPingPongMessage(MessageType.Pong); this.cleatRestartTimeout(); this.restartTimeout = setTimeout(() => { this.restart(); }, 20000); }, pong: () => { this.sendPingPongMessage(MessageType.Ping); this.cleatRestartTimeout(); }, closed: () => { this.cleatRestartTimeout(); }, }, }); } private cleatRestartTimeout() { if (this.restartTimeout) { clearTimeout(this.restartTimeout); } } private onOpen(socket: WebSocket) { this.activeSocket = socket; } private restart() { console.log({ restart: true }); if (this.activeSocket?.readyState === WebSocket.OPEN) { // if the socket is still open for the restart, do the restart this.activeSocket.close(4205, 'Client Restart'); } } private sendPingPongMessage(type: MessageType.Ping | MessageType.Pong) { if (this.activeSocket?.readyState === WebSocket.OPEN) { this.activeSocket.send( stringifyMessage({ type, }) ); } } public request(operation: Operation): Observable { return new Observable((sink) => { return this.client.subscribe( { ...operation, query: print(operation.query as ASTNode) }, { next: sink.next.bind(sink), complete: sink.complete.bind(sink), error: (err) => { if (err instanceof Error) { return sink.error(err); } if (err instanceof CloseEvent) { return sink.error( // reason will be available on clean closes new Error(`Socket closed with event ${err.code} ${err.reason || ''}`) ); } return sink.error( new Error((err as GraphQLError[]).map(({ message }) => message).join(', ')) ); }, } ); }); } } type ASTNode = Parameters[0];