UNPKG

grafserv

Version:

A highly optimized server for GraphQL, powered by Grafast

309 lines 13.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.NodeGrafserv = exports.NodeGrafservBase = void 0; exports.grafserv = grafserv; exports.makeNodeUpgradeHandler = makeNodeUpgradeHandler; const node_querystring_1 = require("node:querystring"); const grafast_1 = require("grafast"); const graphql_ws_1 = require("graphql-ws"); const base_js_1 = require("../../core/base.js"); const utils_js_1 = require("../../utils.js"); const websocketKeepalive_js_1 = require("../../websocketKeepalive.js"); class NodeGrafservBase extends base_js_1.GrafservBase { constructor(config) { super(config); } getDigest(dynamicOptions, req, res, isHTTPS) { const reqUrl = req.url; const qi = reqUrl.indexOf("?"); const path = qi >= 0 ? reqUrl.substring(0, qi) : reqUrl; const search = qi >= 0 ? reqUrl.substring(qi + 1) : null; return { httpVersionMajor: req.httpVersionMajor, httpVersionMinor: req.httpVersionMinor, isSecure: isHTTPS, method: req.method, path, headers: (0, utils_js_1.processHeaders)(req.headers), getQueryParams() { const queryParams = search ? (0, node_querystring_1.parse)(search) : Object.create(null); return queryParams; }, getBody() { return (0, utils_js_1.getBodyFromRequest)(req, dynamicOptions.maxRequestLength); }, requestContext: { node: { req, res, }, }, }; } /** * @deprecated Please user serv.addTo instead, so that websockets can be automatically supported */ createHandler(isHTTPS = false) { return this._createHandler(isHTTPS); } _createHandler(isHTTPS = false) { const dynamicOptions = this.dynamicOptions; return async (req, res, next) => { try { const request = this.getDigest(dynamicOptions, req, res, isHTTPS); const result = await this.processRequest(request); if (result === null) { if (typeof next === "function") { return next(); } else { const payload = Buffer.from(`Could not process ${req.method} request to ${req.url} ─ please POST requests to ${dynamicOptions.graphqlPath}`, "utf8"); res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8", "Content-Length": payload.length, }); res.end(payload); return; } } switch (result.type) { case "error": { // TODO: return error in the format the browser would prefer (JSON, HTML, text) // TODO: respect result.headers if (result.error instanceof grafast_1.SafeError) { const payload = Buffer.from(result.error.message, "utf8"); res.writeHead(result.statusCode, { "Content-Type": "text/plain; charset=utf-8", "Content-Length": payload.length, }); res.end(payload); return; } else if (typeof next === "function") { return next(result.error); } else { // TODO: catch all the code paths that lead here! console.error(result.error); const payload = Buffer.from("An error occurred", "utf8"); res.writeHead(result.statusCode, { "Content-Type": "text/plain; charset=utf-8", "Content-Length": payload.length, }); res.end(payload); return; } } case "buffer": { const { statusCode, headers, buffer } = result; res.writeHead(statusCode, headers); res.end(buffer); return; } case "json": { const { statusCode, headers, json } = result; const buffer = Buffer.from(JSON.stringify(json), "utf8"); headers["Content-Length"] = String(buffer.length); res.writeHead(statusCode, headers); res.end(buffer); return; } case "noContent": { const { statusCode, headers } = result; res.writeHead(statusCode, headers); res.end(); return; } case "bufferStream": { const { statusCode, headers, lowLatency, bufferIterator } = result; if (lowLatency) { req.socket.setTimeout(0); req.socket.setNoDelay(true); req.socket.setKeepAlive(true); } res.writeHead(statusCode, headers); // Clean up when connection closes. const cleanup = () => { try { bufferIterator.return?.(); } catch { /* nom nom nom */ } req.removeListener("close", cleanup); req.removeListener("finish", cleanup); req.removeListener("error", cleanup); }; req.on("close", cleanup); req.on("finish", cleanup); req.on("error", cleanup); // https://github.com/expressjs/compression#server-sent-events const flush = lowLatency ? typeof res.flush === "function" ? res.flush.bind(res) : typeof res.flushHeaders === "function" ? res.flushHeaders.bind(res) : null : null; try { for await (const buffer of bufferIterator) { const bufferIsBelowWatermark = res.write(buffer); if (flush) { flush(); } if (!bufferIsBelowWatermark) { // Wait for drain before pumping more data through await new Promise((resolve) => res.once("drain", resolve)); } } } catch (e) { console.error(`Error occurred during stream; swallowing error.`, e); } finally { res.end(); } return; } default: { const never = result; console.log("Unhandled:"); console.dir(never); const payload = Buffer.from("Server hasn't implemented this yet", "utf8"); res.writeHead(501, { "Content-Length": payload.length }); res.end(payload); return; } } } catch (e) { console.error("Unexpected error occurred:"); console.error(e); if (typeof next === "function") { next(e); } else { const text = "Unknown error occurred"; res.writeHead(500, { "Content-Type": "text/plain", "Content-Length": text.length, }); res.end(text); } } }; } async getUpgradeHandler() { if (this.resolvedPreset.grafserv?.websockets) { return makeNodeUpgradeHandler(this); } else { return null; } } shouldHandleUpgrade(req, _socket, _head) { const fullUrl = req.url; if (!fullUrl) { return false; } const q = fullUrl.indexOf("?"); const url = q >= 0 ? fullUrl.substring(0, q) : fullUrl; const graphqlPath = this.dynamicOptions.graphqlPath; return url === graphqlPath; /* const protocol = req.headers["sec-websocket-protocol"]; const protocols = Array.isArray(protocol) ? protocol : protocol?.split(",").map((p) => p.trim()) ?? []; if (protocols.includes(GRAPHQL_TRANSPORT_WS_PROTOCOL)) ... */ } } exports.NodeGrafservBase = NodeGrafservBase; class NodeGrafserv extends NodeGrafservBase { async addTo(server, addExclusiveWebsocketHandler = true) { const handler = this._createHandler(); server.on("request", handler); this.onRelease(() => { server.off("request", handler); }); // Alias this just to make it easier for users to copy/paste the code below // eslint-disable-next-line @typescript-eslint/no-this-alias const serv = this; if (addExclusiveWebsocketHandler) { const grafservUpgradeHandler = await serv.getUpgradeHandler(); if (grafservUpgradeHandler) { const upgrade = (req, socket, head) => { if (serv.shouldHandleUpgrade(req, socket, head)) { grafservUpgradeHandler(req, socket, head); } else { socket.destroy(); } }; server.on("upgrade", upgrade); serv.onRelease(() => { server.off("upgrade", upgrade); }); } } } } exports.NodeGrafserv = NodeGrafserv; function grafserv(config) { return new NodeGrafserv(config); } async function makeNodeUpgradeHandler(instance) { const ws = await import("ws"); const { WebSocketServer } = ws; const graphqlWsServer = (0, graphql_ws_1.makeServer)((0, utils_js_1.makeGraphQLWSConfig)(instance)); const wsServer = new WebSocketServer({ noServer: true }); const onUpgrade = (req, socket, head) => { wsServer.handleUpgrade(req, socket, head, function done(ws) { wsServer.emit("connection", ws, req); }); }; const onConnection = (socket, request) => { (0, websocketKeepalive_js_1.handleWebSocketKeepalive)(socket, instance.getPreset()); // a new socket opened, let graphql-ws take over const closed = graphqlWsServer.opened({ protocol: socket.protocol, // will be validated send: (data) => new Promise((resolve, reject) => { socket.send(data, (err) => (err ? reject(err) : resolve())); }), // control your data flow by timing the promise resolve close: (code, reason) => socket.close(code, reason), // there are protocol standard closures onMessage: (cb) => socket.on("message", async (event) => { try { // wait for the the operation to complete // - if init message, waits for connect // - if query/mutation, waits for result // - if subscription, waits for complete await cb(event.toString()); } catch (err) { try { // all errors that could be thrown during the // execution of operations will be caught here socket.close(graphql_ws_1.CloseCode.InternalServerError, err.message); } catch { /*noop*/ } } }), }, // pass values to the `extra` field in the context { socket, request }); // notify server that the socket closed socket.once("close", closed); }; wsServer.on("connection", onConnection); instance.onRelease(() => { wsServer.off("connection", onConnection); wsServer.close(); }); return onUpgrade; } //# sourceMappingURL=index.js.map