@httptoolkit/subscriptions-transport-ws
Version:
A websocket transport for GraphQL subscriptions
307 lines • 16.8 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.SubscriptionServer = void 0;
var WebSocket = require("ws");
var message_types_1 = require("./message-types");
var protocol_1 = require("./protocol");
var is_object_1 = require("./utils/is-object");
var graphql_1 = require("graphql");
var empty_iterable_1 = require("./utils/empty-iterable");
var iterall_1 = require("iterall");
var is_subscriptions_1 = require("./utils/is-subscriptions");
var parse_legacy_protocol_1 = require("./legacy/parse-legacy-protocol");
var isWebSocketServer = function (socket) { return socket.on; };
var SubscriptionServer = (function () {
function SubscriptionServer(options, socketOptionsOrServer) {
var _this = this;
var onOperation = options.onOperation, onOperationComplete = options.onOperationComplete, onConnect = options.onConnect, onDisconnect = options.onDisconnect, keepAlive = options.keepAlive;
this.specifiedRules = options.validationRules || graphql_1.specifiedRules;
this.loadExecutor(options);
this.onOperation = onOperation;
this.onOperationComplete = onOperationComplete;
this.onConnect = onConnect;
this.onDisconnect = onDisconnect;
this.keepAlive = keepAlive;
if (isWebSocketServer(socketOptionsOrServer)) {
this.wsServer = socketOptionsOrServer;
}
else {
this.wsServer = new WebSocket.Server(socketOptionsOrServer || {});
}
var connectionHandler = (function (socket, request) {
socket.upgradeReq = request;
if (socket.protocol === undefined ||
(socket.protocol.indexOf(protocol_1.GRAPHQL_WS) === -1 && socket.protocol.indexOf(protocol_1.GRAPHQL_SUBSCRIPTIONS) === -1)) {
socket.close(1002);
return;
}
var connectionContext = Object.create(null);
connectionContext.initPromise = Promise.resolve(true);
connectionContext.isLegacy = false;
connectionContext.socket = socket;
connectionContext.request = request;
connectionContext.operations = {};
var connectionClosedHandler = function (error) {
if (error) {
_this.sendError(connectionContext, '', { message: error.message ? error.message : error }, message_types_1.default.GQL_CONNECTION_ERROR);
setTimeout(function () {
connectionContext.socket.close(1011);
}, 10);
}
_this.onClose(connectionContext);
if (_this.onDisconnect) {
_this.onDisconnect(socket, connectionContext);
}
};
socket.on('error', connectionClosedHandler);
socket.on('close', connectionClosedHandler);
socket.on('message', _this.onMessage(connectionContext));
});
this.wsServer.on('connection', connectionHandler);
this.closeHandler = function () {
_this.wsServer.removeListener('connection', connectionHandler);
_this.wsServer.close();
_this.wsServer.clients.forEach(function (ws) { return ws.terminate(); });
};
}
SubscriptionServer.create = function (options, socketOptionsOrServer) {
return new SubscriptionServer(options, socketOptionsOrServer);
};
Object.defineProperty(SubscriptionServer.prototype, "server", {
get: function () {
return this.wsServer;
},
enumerable: false,
configurable: true
});
SubscriptionServer.prototype.close = function () {
this.closeHandler();
};
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 && connectionContext.operations[opId]) {
if (connectionContext.operations[opId].return) {
connectionContext.operations[opId].return();
}
delete connectionContext.operations[opId];
if (this.onOperationComplete) {
this.onOperationComplete(connectionContext.socket, opId);
}
}
};
SubscriptionServer.prototype.onClose = function (connectionContext) {
var _this = this;
Object.keys(connectionContext.operations).forEach(function (opId) {
_this.unsubscribe(connectionContext, opId);
});
};
SubscriptionServer.prototype.onMessage = function (connectionContext) {
var _this = this;
return function (message) {
var parsedMessage;
try {
parsedMessage = (0, parse_legacy_protocol_1.parseLegacyProtocolMessage)(connectionContext, JSON.parse(message.toString()));
}
catch (e) {
_this.sendError(connectionContext, null, { message: e.message }, message_types_1.default.GQL_CONNECTION_ERROR);
return;
}
var opId = parsedMessage.id;
switch (parsedMessage.type) {
case message_types_1.default.GQL_CONNECTION_INIT:
if (_this.onConnect) {
connectionContext.initPromise = new Promise(function (resolve, reject) {
try {
resolve(_this.onConnect(parsedMessage.payload, connectionContext.socket, connectionContext));
}
catch (e) {
reject(e);
}
});
}
connectionContext.initPromise.then(function (result) {
if (result === false) {
throw new Error('Prohibited connection!');
}
_this.sendMessage(connectionContext, undefined, message_types_1.default.GQL_CONNECTION_ACK, undefined);
if (_this.keepAlive) {
_this.sendKeepAlive(connectionContext);
var keepAliveTimer_1 = setInterval(function () {
if (connectionContext.socket.readyState === WebSocket.OPEN) {
_this.sendKeepAlive(connectionContext);
}
else {
clearInterval(keepAliveTimer_1);
}
}, _this.keepAlive);
}
}).catch(function (error) {
_this.sendError(connectionContext, opId, { message: error.message }, message_types_1.default.GQL_CONNECTION_ERROR);
setTimeout(function () {
connectionContext.socket.close(1011);
}, 10);
});
break;
case message_types_1.default.GQL_CONNECTION_TERMINATE:
connectionContext.socket.close();
break;
case message_types_1.default.GQL_START:
connectionContext.initPromise.then(function (initResult) {
if (connectionContext.operations && connectionContext.operations[opId]) {
_this.unsubscribe(connectionContext, opId);
}
var baseParams = {
query: parsedMessage.payload.query,
variables: parsedMessage.payload.variables,
operationName: parsedMessage.payload.operationName,
context: (0, is_object_1.default)(initResult) ? Object.assign(Object.create(Object.getPrototypeOf(initResult)), initResult) : {},
formatResponse: undefined,
formatError: undefined,
callback: undefined,
schema: _this.schema,
};
var promisedParams = Promise.resolve(baseParams);
connectionContext.operations[opId] = (0, empty_iterable_1.createEmptyIterable)();
if (_this.onOperation) {
var messageForCallback = parsedMessage;
promisedParams = Promise.resolve(_this.onOperation(messageForCallback, baseParams, connectionContext.socket));
}
return promisedParams.then(function (params) {
if (typeof params !== 'object') {
var error = "Invalid params returned from onOperation! return values must be an object!";
_this.sendError(connectionContext, opId, { message: error });
throw new Error(error);
}
if (!params.schema) {
var 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!';
_this.sendError(connectionContext, opId, { message: error });
throw new Error(error);
}
var document = typeof baseParams.query !== 'string' ? baseParams.query : (0, graphql_1.parse)(baseParams.query);
var executionPromise;
var validationErrors = (0, graphql_1.validate)(params.schema, document, _this.specifiedRules);
if (validationErrors.length > 0) {
executionPromise = Promise.resolve({ errors: validationErrors });
}
else {
var executor = _this.execute;
if (_this.subscribe && (0, is_subscriptions_1.isASubscriptionOperation)(document, params.operationName)) {
executor = _this.subscribe;
}
executionPromise = Promise.resolve(executor({
schema: params.schema,
document: document,
rootValue: _this.rootValue,
contextValue: params.context,
variableValues: params.variables,
operationName: params.operationName,
}));
}
return executionPromise.then(function (executionResult) { return ({
executionIterable: (0, iterall_1.isAsyncIterable)(executionResult) ?
executionResult : (0, iterall_1.createAsyncIterator)([executionResult]),
params: params,
}); });
}).then(function (_a) {
var executionIterable = _a.executionIterable, params = _a.params;
(0, iterall_1.forAwaitEach)(executionIterable, function (value) {
var result = value;
if (params.formatResponse) {
try {
result = params.formatResponse(value, params);
}
catch (err) {
console.error('Error in formatResponse function:', err);
}
}
_this.sendMessage(connectionContext, opId, message_types_1.default.GQL_DATA, result);
})
.then(function () {
_this.sendMessage(connectionContext, opId, message_types_1.default.GQL_COMPLETE, null);
})
.catch(function (e) {
var error = e;
if (params.formatError) {
try {
error = params.formatError(e, params);
}
catch (err) {
console.error('Error in formatError function: ', err);
}
}
if (Object.keys(error).length === 0) {
error = { name: error.name, message: error.message };
}
_this.sendError(connectionContext, opId, error);
});
return executionIterable;
}).then(function (subscription) {
connectionContext.operations[opId] = subscription;
}).then(function () {
_this.sendMessage(connectionContext, opId, message_types_1.default.SUBSCRIPTION_SUCCESS, undefined);
}).catch(function (e) {
if (e.errors) {
_this.sendMessage(connectionContext, opId, message_types_1.default.GQL_DATA, { errors: e.errors });
}
else {
_this.sendError(connectionContext, opId, { message: e.message });
}
_this.unsubscribe(connectionContext, opId);
return;
});
}).catch(function (error) {
_this.sendError(connectionContext, opId, { message: error.message });
_this.unsubscribe(connectionContext, opId);
});
break;
case message_types_1.default.GQL_STOP:
_this.unsubscribe(connectionContext, opId);
break;
default:
_this.sendError(connectionContext, opId, { message: 'Invalid message type!' });
}
};
};
SubscriptionServer.prototype.sendKeepAlive = function (connectionContext) {
if (connectionContext.isLegacy) {
this.sendMessage(connectionContext, undefined, message_types_1.default.KEEP_ALIVE, undefined);
}
else {
this.sendMessage(connectionContext, undefined, message_types_1.default.GQL_CONNECTION_KEEP_ALIVE, undefined);
}
};
SubscriptionServer.prototype.sendMessage = function (connectionContext, opId, type, payload) {
var parsedMessage = (0, parse_legacy_protocol_1.parseLegacyProtocolMessage)(connectionContext, {
type: type,
id: opId,
payload: payload,
});
if (parsedMessage && connectionContext.socket.readyState === WebSocket.OPEN) {
connectionContext.socket.send(JSON.stringify(parsedMessage));
}
};
SubscriptionServer.prototype.sendError = function (connectionContext, opId, errorPayload, overrideDefaultErrorType) {
var sanitizedOverrideDefaultErrorType = overrideDefaultErrorType || message_types_1.default.GQL_ERROR;
if ([
message_types_1.default.GQL_CONNECTION_ERROR,
message_types_1.default.GQL_ERROR,
].indexOf(sanitizedOverrideDefaultErrorType) === -1) {
throw new Error('overrideDefaultErrorType should be one of the allowed error messages' +
' GQL_CONNECTION_ERROR or GQL_ERROR');
}
this.sendMessage(connectionContext, opId, sanitizedOverrideDefaultErrorType, errorPayload);
};
return SubscriptionServer;
}());
exports.SubscriptionServer = SubscriptionServer;
//# sourceMappingURL=server.js.map
;