UNPKG

@fanoutio/subscriptions-transport-ws-over-http

Version:
655 lines 37.9 kB
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __generator = (this && this.__generator) || function (thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (_) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } }; var __asyncValues = (this && this.__asyncValues) || function (o) { if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined."); var m = o[Symbol.asyncIterator], i; return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i); function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; } function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); } }; import { WebSocketMessageFormat, } from "@fanoutio/grip"; import { ServeGrip, } from '@fanoutio/serve-grip'; import { parse, validate, specifiedRules, } from 'graphql'; import { installHttpRequestHandler } from "../utils/httpServer"; import MessageTypes from "../data/message-types"; import isObject, { isASubscriptionOperation } from "../utils/types"; import { createEmptyIterable } from "../utils/empty-iterable"; import { createAsyncIterator, isAsyncIterable } from "iterall"; import { TempDirectoryPersistence } from "../persistence/fs-impl"; var CHANNEL_NAME = "test"; var SubscriptionServer = /** @class */ (function () { function SubscriptionServer(options, server) { var _a; this._connectionContexts = {}; var onOperation = options.onOperation, onOperationComplete = options.onOperationComplete, onConnect = options.onConnect, onDisconnect = options.onDisconnect, keepAlive = options.keepAlive, grip = options.grip, gripPrefix = options.gripPrefix, persistence = options.persistence; this.specifiedRules = (_a = options.validationRules) !== null && _a !== void 0 ? _a : specifiedRules; this.loadExecutor(options); this.onOperation = onOperation; this.onOperationComplete = onOperationComplete; this.onConnect = onConnect; this.onDisconnect = onDisconnect; this.keepAlive = keepAlive; this.server = server; this.serveGrip = new ServeGrip({ grip: grip, prefix: gripPrefix, }); if (persistence != null) { this.persistence = persistence; } else { this.persistence = new TempDirectoryPersistence(); } this.readyPromise = this.prep(); } SubscriptionServer.create = function (options, server) { return new SubscriptionServer(options, server); }; SubscriptionServer.prototype.requestHandler = function (req, res) { return __awaiter(this, void 0, void 0, function () { var gripReq, gripRes, wsContext, connectionContext, connectionClosedHandler, onMessage, message, ex_1; var _this = this; return __generator(this, function (_a) { switch (_a.label) { case 0: // Make sure we are ready, including rehydration. // We actually have to do this NOW, since returning false here can still // go to code paths where mutations will occur and push updates to subscriptions. return [4 /*yield*/, this.ready()]; case 1: // Make sure we are ready, including rehydration. // We actually have to do this NOW, since returning false here can still // go to code paths where mutations will occur and push updates to subscriptions. _a.sent(); gripReq = req; gripRes = res; return [4 /*yield*/, this.serveGrip.run(gripReq, gripRes)]; case 2: // If this return false, then it means serveGrip has handled the response if (!(_a.sent())) { return [2 /*return*/, true]; } wsContext = gripReq.grip.wsContext; if (wsContext == null) { return [2 /*return*/, false]; } // Even if this is a websocket request, we only accept this if // we are using the graphql-ws subprotocol. // This function returns false if it was unable to accept the protocol. if (!wsContext.acceptSubprotocol('graphql-ws')) { return [2 /*return*/, false]; } connectionContext = this.getConnectionContext(wsContext.id); connectionContext.wsContext = wsContext; connectionContext.request = req; connectionClosedHandler = function (error) { return __awaiter(_this, void 0, void 0, function () { return __generator(this, function (_a) { switch (_a.label) { case 0: console.log("connection closed handler"); if (!(error != null)) return [3 /*break*/, 2]; console.log("error", error); return [4 /*yield*/, this.sendError(connectionContext, '', { message: error.message ? error.message : error }, MessageTypes.GQL_CONNECTION_ERROR)]; case 1: _a.sent(); _a.label = 2; case 2: this.onClose(wsContext, connectionContext); return [4 /*yield*/, this.persistence.purgePersistedMessage(wsContext.id)]; case 3: _a.sent(); if (error != null) { // 1011 is an unexpected condition prevented the request from being fulfilled wsContext.close(1011); } else { console.log("wsContext is closing"); wsContext.close(); } return [2 /*return*/]; } }); }); }; onMessage = this.onMessage(connectionContext); if (wsContext.isOpening()) { console.log("wsContext is Opening"); wsContext.accept(); wsContext.subscribe(CHANNEL_NAME + wsContext.id); } _a.label = 3; case 3: if (!wsContext.canRecv()) return [3 /*break*/, 12]; message = void 0; _a.label = 4; case 4: _a.trys.push([4, 5, , 7]); message = wsContext.recv(); return [3 /*break*/, 7]; case 5: ex_1 = _a.sent(); return [4 /*yield*/, connectionClosedHandler(ex_1)]; case 6: _a.sent(); return [3 /*break*/, 12]; case 7: if (!(message == null)) return [3 /*break*/, 9]; // If return value is null then connection is closed return [4 /*yield*/, connectionClosedHandler()]; case 8: // If return value is null then connection is closed _a.sent(); return [3 /*break*/, 12]; case 9: return [4 /*yield*/, this.persistence.persistMessage(wsContext.id, message)]; case 10: _a.sent(); return [4 /*yield*/, onMessage(message)]; case 11: _a.sent(); return [3 /*break*/, 3]; case 12: console.log("Calling res.end"); res.end(); connectionContext.wsContext = undefined; return [2 /*return*/, true]; } }); }); }; SubscriptionServer.prototype.prep = function () { return __awaiter(this, void 0, void 0, function () { var persistedMessages, _i, _a, _b, wsId, messages, connectionContext, onMessage, _c, messages_1, message; var _this = this; return __generator(this, function (_d) { switch (_d.label) { case 0: return [4 /*yield*/, this.persistence.getPersistedMessages()]; case 1: persistedMessages = _d.sent(); _i = 0, _a = Object.entries(persistedMessages); _d.label = 2; case 2: if (!(_i < _a.length)) return [3 /*break*/, 7]; _b = _a[_i], wsId = _b[0], messages = _b[1]; console.log("Replaying messages for", wsId); connectionContext = this.getConnectionContext(wsId); onMessage = this.onMessage(connectionContext); _c = 0, messages_1 = messages; _d.label = 3; case 3: if (!(_c < messages_1.length)) return [3 /*break*/, 6]; message = messages_1[_c]; console.log("Replaying message", message); return [4 /*yield*/, onMessage(message)]; case 4: _d.sent(); _d.label = 5; case 5: _c++; return [3 /*break*/, 3]; case 6: _i++; return [3 /*break*/, 2]; case 7: if (this.server != null) { installHttpRequestHandler(this.server, function (req, res) { return __awaiter(_this, void 0, void 0, function () { return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, this.requestHandler(req, res)]; case 1: return [2 /*return*/, _a.sent()]; } }); }); }); } return [2 /*return*/]; } }); }); }; SubscriptionServer.prototype.ready = function () { return this.readyPromise; }; SubscriptionServer.prototype.getConnectionContext = function (wsId) { if (!(wsId in this._connectionContexts)) { var connectionContext = Object.create(null); connectionContext.initPromise = Promise.resolve(true); connectionContext.wsId = wsId; connectionContext.operations = {}; this._connectionContexts[wsId] = connectionContext; } return this._connectionContexts[wsId]; }; SubscriptionServer.prototype.loadExecutor = function (options) { var execute = options.execute, subscribe = options.subscribe, schema = options.schema, rootValue = options.rootValue; if (!execute) { throw new Error('Must provide `execute` for websocket server constructor.'); } this.schema = schema; this.rootValue = rootValue; this.execute = execute; this.subscribe = subscribe; }; SubscriptionServer.prototype.unsubscribe = function (connectionContext, opId) { if (connectionContext.operations != null && connectionContext.operations[opId] != null) { if (connectionContext.operations[opId].return != null) { connectionContext.operations[opId].return(); } console.log("removing subscription", opId); delete connectionContext.operations[opId]; if (this.onOperationComplete) { this.onOperationComplete(connectionContext.wsId, opId); } } }; SubscriptionServer.prototype.onClose = function (wsContext, connectionContext) { var _this = this; Object.keys(connectionContext.operations).forEach(function (opId) { _this.unsubscribe(connectionContext, opId); }); if (this.onDisconnect) { this.onDisconnect(wsContext.id, connectionContext); } }; SubscriptionServer.prototype.onMessage = function (connectionContext) { var _this = this; return function (message) { return __awaiter(_this, void 0, void 0, function () { var parsedMessage, e_1, wsContext, wsId, opId, _a, initResult_1, keepAliveTimer_1, error_1, initResult, error_2, params_1, error, error, document_1, validationErrors, isSubscription, executionResult, executor, executionIterable_1, iterateSubscriptions, e_2; var _this = this; var _b, _c, _d; return __generator(this, function (_e) { switch (_e.label) { case 0: console.log("message", message); _e.label = 1; case 1: _e.trys.push([1, 2, , 4]); parsedMessage = JSON.parse(message); return [3 /*break*/, 4]; case 2: e_1 = _e.sent(); return [4 /*yield*/, this.sendError(connectionContext, undefined, { message: e_1.message }, MessageTypes.GQL_CONNECTION_ERROR)]; case 3: _e.sent(); return [2 /*return*/]; case 4: console.log("parsedMessage", parsedMessage); wsContext = connectionContext.wsContext; wsId = wsContext === null || wsContext === void 0 ? void 0 : wsContext.id; console.log("wsContext: ", wsId); opId = parsedMessage.id; console.log("opId", opId); _a = parsedMessage.type; switch (_a) { case MessageTypes.GQL_CONNECTION_INIT: return [3 /*break*/, 5]; case MessageTypes.GQL_CONNECTION_TERMINATE: return [3 /*break*/, 14]; case MessageTypes.GQL_START: return [3 /*break*/, 15]; case MessageTypes.GQL_STOP: return [3 /*break*/, 40]; } return [3 /*break*/, 41]; case 5: console.log("GQL_CONNECTION_INIT"); _e.label = 6; case 6: _e.trys.push([6, 11, , 13]); if (this.onConnect != null) { connectionContext.initPromise = this.onConnect(parsedMessage.payload, wsId, connectionContext); } return [4 /*yield*/, connectionContext.initPromise]; case 7: initResult_1 = _e.sent(); if (initResult_1 === false) { throw new Error('Prohibited connection!'); } return [4 /*yield*/, this.sendMessage(connectionContext, undefined, MessageTypes.GQL_CONNECTION_ACK, undefined)]; case 8: _e.sent(); if (!this.keepAlive) return [3 /*break*/, 10]; return [4 /*yield*/, this.sendKeepAlive(connectionContext)]; case 9: _e.sent(); keepAliveTimer_1 = setInterval(function () { // Make sure this connection is still alive. if (connectionContext.wsContext != null) { _this.sendKeepAlive(connectionContext); } else { clearInterval(keepAliveTimer_1); } }, this.keepAlive); _e.label = 10; case 10: return [3 /*break*/, 13]; case 11: error_1 = _e.sent(); return [4 /*yield*/, this.sendError(connectionContext, opId, { message: error_1.message }, MessageTypes.GQL_CONNECTION_ERROR)]; case 12: _e.sent(); // Close the connection with an error code, ws v2 ensures that the // connection is cleaned up even when the closing handshake fails. // 1011: an unexpected condition prevented the operation from being fulfilled wsContext === null || wsContext === void 0 ? void 0 : wsContext.close(1011); return [3 /*break*/, 13]; case 13: return [3 /*break*/, 43]; case 14: console.log("GQL_CONNECTION_TERMINATE"); this.onClose(wsContext, connectionContext); wsContext === null || wsContext === void 0 ? void 0 : wsContext.close(); return [3 /*break*/, 43]; case 15: console.log("GQL_START"); initResult = void 0; _e.label = 16; case 16: _e.trys.push([16, 18, , 20]); return [4 /*yield*/, connectionContext.initPromise]; case 17: // this can fail initResult = _e.sent(); console.log("initResult", initResult); return [3 /*break*/, 20]; case 18: error_2 = _e.sent(); // Handle initPromise rejected return [4 /*yield*/, this.sendError(connectionContext, opId, { message: error_2.message })]; case 19: // Handle initPromise rejected _e.sent(); this.unsubscribe(connectionContext, opId); console.log("error 1"); return [3 /*break*/, 20]; case 20: // if we already have a subscription with this id, unsubscribe from it first if (connectionContext.operations != null && connectionContext.operations[opId] != null) { console.log("unsubscribing", opId); this.unsubscribe(connectionContext, opId); } // set an initial mock subscription to only registering opId connectionContext.operations[opId] = createEmptyIterable(); params_1 = { query: (_b = parsedMessage.payload) === null || _b === void 0 ? void 0 : _b.query, variables: (_c = parsedMessage.payload) === null || _c === void 0 ? void 0 : _c.variables, operationName: (_d = parsedMessage.payload) === null || _d === void 0 ? void 0 : _d.operationName, context: isObject(initResult) ? Object.assign(Object.create(Object.getPrototypeOf(initResult)), initResult) : {}, formatResponse: undefined, formatError: undefined, schema: this.schema, }; console.log("created params"); _e.label = 21; case 21: _e.trys.push([21, 34, , 39]); if (!this.onOperation) return [3 /*break*/, 23]; return [4 /*yield*/, this.onOperation(message, params_1, wsId)]; case 22: params_1 = _e.sent(); _e.label = 23; case 23: if (!(typeof params_1 !== 'object')) return [3 /*break*/, 25]; error = "Invalid params returned from onOperation! return values must be an object!"; return [4 /*yield*/, this.sendError(connectionContext, opId, { message: error })]; case 24: _e.sent(); throw new Error(error); case 25: if (!!params_1.schema) return [3 /*break*/, 27]; error = 'Missing schema information. The GraphQL schema should be provided either statically in' + ' the `SubscriptionServer` constructor or as a property on the object returned from onOperation!'; return [4 /*yield*/, this.sendError(connectionContext, opId, { message: error })]; case 26: _e.sent(); console.log("no schema"); throw new Error(error); case 27: console.log("after schema check"); console.log("params", params_1); document_1 = typeof params_1.query !== 'string' ? params_1.query : parse(params_1.query); console.log("document", document_1); validationErrors = validate(params_1.schema, document_1, this.specifiedRules); console.log("after validate()"); isSubscription = false; executionResult = void 0; if (!(validationErrors.length > 0)) return [3 /*break*/, 28]; executionResult = { errors: validationErrors }; console.log("validation errors"); return [3 /*break*/, 30]; case 28: executor = void 0; if (this.subscribe && isASubscriptionOperation(document_1, params_1.operationName)) { isSubscription = true; executor = this.subscribe; } else { executor = this.execute; } console.log("executing", document_1, params_1); return [4 /*yield*/, executor(params_1.schema, document_1, this.rootValue, params_1.context, params_1.variables, params_1.operationName)]; case 29: executionResult = _e.sent(); console.log("executed", executionResult); _e.label = 30; case 30: executionIterable_1 = isAsyncIterable(executionResult) ? executionResult : createAsyncIterator([executionResult]); console.log("executionIterable", executionIterable_1); iterateSubscriptions = function () { return __awaiter(_this, void 0, void 0, function () { var executionIterable_2, executionIterable_2_1, value, result, e_3_1, e_4, error; var e_3, _a; return __generator(this, function (_b) { switch (_b.label) { case 0: _b.trys.push([0, 15, , 17]); console.log("before iterating"); _b.label = 1; case 1: _b.trys.push([1, 7, 8, 13]); executionIterable_2 = __asyncValues(executionIterable_1); _b.label = 2; case 2: return [4 /*yield*/, executionIterable_2.next()]; case 3: if (!(executionIterable_2_1 = _b.sent(), !executionIterable_2_1.done)) return [3 /*break*/, 6]; value = executionIterable_2_1.value; console.log("iterating"); result = value; if (params_1.formatResponse) { try { result = params_1.formatResponse(value, params_1); } catch (err) { console.error('Error in formatResponse function:', err); } } return [4 /*yield*/, this.sendMessage(connectionContext, opId, MessageTypes.GQL_DATA, result)]; case 4: _b.sent(); _b.label = 5; case 5: return [3 /*break*/, 2]; case 6: return [3 /*break*/, 13]; case 7: e_3_1 = _b.sent(); e_3 = { error: e_3_1 }; return [3 /*break*/, 13]; case 8: _b.trys.push([8, , 11, 12]); if (!(executionIterable_2_1 && !executionIterable_2_1.done && (_a = executionIterable_2.return))) return [3 /*break*/, 10]; return [4 /*yield*/, _a.call(executionIterable_2)]; case 9: _b.sent(); _b.label = 10; case 10: return [3 /*break*/, 12]; case 11: if (e_3) throw e_3.error; return [7 /*endfinally*/]; case 12: return [7 /*endfinally*/]; case 13: console.log("iterated"); return [4 /*yield*/, this.sendMessage(connectionContext, opId, MessageTypes.GQL_COMPLETE, null)]; case 14: _b.sent(); return [3 /*break*/, 17]; case 15: e_4 = _b.sent(); console.log("error after iteration"); error = e_4; if (params_1.formatError) { try { error = params_1.formatError(e_4, params_1); } catch (err) { console.error('Error in formatError function: ', err); } } // plain Error object cannot be JSON stringified. if (Object.keys(error).length === 0) { error = { name: error.name, message: error.message }; } return [4 /*yield*/, this.sendError(connectionContext, opId, error)]; case 16: _b.sent(); return [3 /*break*/, 17]; case 17: return [2 /*return*/]; } }); }); }; if (!isSubscription) return [3 /*break*/, 31]; console.log("is subscription -- not awaiting iteration."); iterateSubscriptions(); return [3 /*break*/, 33]; case 31: console.log("is execution -- awaiting iteration."); return [4 /*yield*/, iterateSubscriptions()]; case 32: _e.sent(); _e.label = 33; case 33: console.log("adding subscription", opId); connectionContext.operations[opId] = executionIterable_1; return [3 /*break*/, 39]; case 34: e_2 = _e.sent(); console.log("error 3"); if (!e_2.errors) return [3 /*break*/, 36]; return [4 /*yield*/, this.sendMessage(connectionContext, opId, MessageTypes.GQL_DATA, { errors: e_2.errors })]; case 35: _e.sent(); return [3 /*break*/, 38]; case 36: return [4 /*yield*/, this.sendError(connectionContext, opId, { message: e_2.message })]; case 37: _e.sent(); _e.label = 38; case 38: // Remove the operation on the server side as it will be removed also in the client this.unsubscribe(connectionContext, opId); return [3 /*break*/, 39]; case 39: return [3 /*break*/, 43]; case 40: console.log("GQL_STOP"); // Find subscription id. Call unsubscribe. this.unsubscribe(connectionContext, opId); return [3 /*break*/, 43]; case 41: console.log("unknown message type"); return [4 /*yield*/, this.sendError(connectionContext, opId, { message: 'Invalid message type!' })]; case 42: _e.sent(); _e.label = 43; case 43: return [2 /*return*/]; } }); }); }; }; SubscriptionServer.prototype.sendKeepAlive = function (connectionContext) { return __awaiter(this, void 0, void 0, function () { return __generator(this, function (_a) { switch (_a.label) { case 0: console.log("sendKeepAlive", connectionContext.wsId); return [4 /*yield*/, this.sendMessage(connectionContext, undefined, MessageTypes.GQL_CONNECTION_KEEP_ALIVE, undefined)]; case 1: _a.sent(); return [2 /*return*/]; } }); }); }; SubscriptionServer.prototype.sendMessage = function (connectionContext, opId, type, payload) { return __awaiter(this, void 0, void 0, function () { var parsedMessage, message, publisher; return __generator(this, function (_a) { switch (_a.label) { case 0: parsedMessage = { type: type, id: opId, payload: payload, }; console.log("sendMessage", connectionContext.wsId, JSON.stringify(parsedMessage, null, 2)); message = JSON.stringify(parsedMessage); if (!(connectionContext.wsContext != null)) return [3 /*break*/, 1]; console.log("wsContext not null, sending directly", connectionContext.wsContext.id); connectionContext.wsContext.send(message); console.log("sent directly", connectionContext.wsContext.id); return [3 /*break*/, 3]; case 1: console.log("wsContext null, sending through GRIP", connectionContext.wsId); publisher = this.serveGrip.getPublisher(); return [4 /*yield*/, publisher.publishFormats(CHANNEL_NAME + connectionContext.wsId, new WebSocketMessageFormat(message))]; case 2: _a.sent(); console.log("sent through GRIP", connectionContext.wsId); _a.label = 3; case 3: return [2 /*return*/]; } }); }); }; SubscriptionServer.prototype.sendError = function (connectionContext, opId, errorPayload, overrideDefaultErrorType) { return __awaiter(this, void 0, void 0, function () { var sanitizedOverrideDefaultErrorType; return __generator(this, function (_a) { switch (_a.label) { case 0: sanitizedOverrideDefaultErrorType = overrideDefaultErrorType || MessageTypes.GQL_ERROR; if ([ MessageTypes.GQL_CONNECTION_ERROR, MessageTypes.GQL_ERROR, ].indexOf(sanitizedOverrideDefaultErrorType) === -1) { throw new Error('overrideDefaultErrorType should be one of the allowed error messages' + ' GQL_CONNECTION_ERROR or GQL_ERROR'); } return [4 /*yield*/, this.sendMessage(connectionContext, opId, sanitizedOverrideDefaultErrorType, errorPayload)]; case 1: _a.sent(); return [2 /*return*/]; } }); }); }; return SubscriptionServer; }()); export { SubscriptionServer }; //# sourceMappingURL=server.js.map