UNPKG

@graphql-tools/executor-legacy-ws

Version:

A set of utils for faster development of GraphQL tools

189 lines (188 loc) • 7.42 kB
import { print } from 'graphql'; import WebSocket from 'isomorphic-ws'; import { observableToAsyncIterable, } from '@graphql-tools/utils'; export var LEGACY_WS; (function (LEGACY_WS) { LEGACY_WS["CONNECTION_INIT"] = "connection_init"; LEGACY_WS["CONNECTION_ACK"] = "connection_ack"; LEGACY_WS["CONNECTION_ERROR"] = "connection_error"; LEGACY_WS["CONNECTION_KEEP_ALIVE"] = "ka"; LEGACY_WS["START"] = "start"; LEGACY_WS["STOP"] = "stop"; LEGACY_WS["CONNECTION_TERMINATE"] = "connection_terminate"; LEGACY_WS["DATA"] = "data"; LEGACY_WS["ERROR"] = "error"; LEGACY_WS["COMPLETE"] = "complete"; })(LEGACY_WS || (LEGACY_WS = {})); function isConnectionParamsRecord(value) { return value != null && typeof value === 'object' && !Array.isArray(value); } export function buildWSLegacyExecutor(subscriptionsEndpoint, WebSocketImpl, options) { const disposers = new Set(); let operationSeq = 0; function baseConnectionParams() { const connectionParams = options?.connectionParams; if (typeof connectionParams === 'function') { const resolved = connectionParams(); return isConnectionParamsRecord(resolved) ? { ...resolved } : {}; } if (isConnectionParamsRecord(connectionParams)) { return { ...connectionParams }; } return {}; } function requestConnectionParams(request) { const fromExtensions = request.extensions?.['connectionParams']; return isConnectionParamsRecord(fromExtensions) ? { ...fromExtensions } : {}; } const executor = function legacyExecutor(request) { // Bound to this execution only. A later request must not inherit keys it omits. const connectionParams = { ...baseConnectionParams(), ...requestConnectionParams(request), }; operationSeq += 1; const id = `${operationSeq.toString(36)}-${Math.random().toString(36).slice(2)}`; return observableToAsyncIterable({ subscribe(observer) { let closed = false; const websocket = new WebSocketImpl(subscriptionsEndpoint, 'graphql-ws', { followRedirects: true, headers: options?.headers, rejectUnauthorized: options?.rejectUnauthorized ?? true, skipUTF8Validation: true, }); function closeSocket(sendStop) { if (closed) { return; } closed = true; disposers.delete(disposeSocket); if (websocket.readyState === WebSocket.OPEN) { if (sendStop) { websocket.send(JSON.stringify({ type: LEGACY_WS.STOP, id, })); } websocket.send(JSON.stringify({ type: LEGACY_WS.CONNECTION_TERMINATE, })); } websocket.terminate(); } function endWithError(err) { if (closed) { return; } closeSocket(false); observer.error(err); observer.complete(); } function disposeSocket() { if (closed) { return; } closeSocket(false); observer.complete(); } disposers.add(disposeSocket); websocket.onopen = () => { if (closed) { return; } websocket.send(JSON.stringify({ type: LEGACY_WS.CONNECTION_INIT, payload: connectionParams, }), (error) => { if (error) { endWithError(error); } }); }; websocket.onerror = event => { endWithError(event.error); }; websocket.onclose = () => { if (closed) { return; } closed = true; disposers.delete(disposeSocket); observer.complete(); }; websocket.onmessage = event => { if (closed) { return; } let data; try { data = JSON.parse(event.data.toString('utf-8')); } catch (error) { endWithError(error); return; } switch (data.type) { case LEGACY_WS.CONNECTION_ACK: { websocket.send(JSON.stringify({ type: LEGACY_WS.START, id, payload: { query: print(request.document), variables: request.variables, operationName: request.operationName, }, }), (error) => { if (error) { endWithError(error); } }); break; } case LEGACY_WS.CONNECTION_ERROR: { endWithError(data.payload); break; } case LEGACY_WS.CONNECTION_KEEP_ALIVE: { break; } case LEGACY_WS.DATA: { if (data.id !== id) { break; } observer.next(data.payload); break; } case LEGACY_WS.ERROR: { if (data.id !== id) { break; } endWithError(data.payload); break; } case LEGACY_WS.COMPLETE: { if (data.id !== id) { break; } closeSocket(false); observer.complete(); break; } } }; return { unsubscribe: () => { closeSocket(true); }, }; }, }); }; executor[Symbol.dispose] = () => { for (const disposeSocket of [...disposers]) { disposeSocket(); } }; return executor; }