UNPKG

@aws-amplify/amplify-appsync-simulator

Version:

An AppSync Simulator to test AppSync API.

236 lines 11.1 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.WebsocketSubscriptionServer = exports.REALTIME_SUBSCRIPTION_PATH = void 0; const graphql_1 = require("graphql"); const WebSocket = __importStar(require("ws")); const ws_1 = require("ws"); const message_type_guards_1 = require("./message-type-guards"); const message_types_1 = require("./message-types"); const utils_1 = require("./utils"); exports.REALTIME_SUBSCRIPTION_PATH = '/graphql/realtime'; const PROTOCOL = 'graphql-ws'; const KEEP_ALIVE_TIMEOUT = 4 * 60 * 1000; const CONNECTION_TIMEOUT_DURATION = 5 * 60 * 1000; const DEFAULT_OPTIONS = { onConnectHandler: async () => { }, keepAlive: KEEP_ALIVE_TIMEOUT, connectionTimeoutDuration: CONNECTION_TIMEOUT_DURATION, }; class WebsocketSubscriptionServer { constructor(options, server) { this.onClose = async (connectionContext) => { for (const subscription of Array.from(connectionContext.subscriptions.values())) { await this.stopAsyncIterator(connectionContext, subscription.id); } if (connectionContext.pingIntervalHandle) { clearInterval(connectionContext.pingIntervalHandle); connectionContext.pingIntervalHandle = null; } this.connections.delete(connectionContext); }; this.onUnsubscribe = async (connectionContext, messageOrSubscriptionId) => { const { id } = messageOrSubscriptionId; await this.stopAsyncIterator(connectionContext, id); this.sendMessage(connectionContext, id, message_types_1.MESSAGE_TYPES.GQL_COMPLETE, {}); }; this.stopAsyncIterator = async (connectionContext, id) => { if (connectionContext.subscriptions && connectionContext.subscriptions.has(id)) { const subscription = connectionContext.subscriptions.get(id); if (subscription.asyncIterator) { await subscription.asyncIterator.return(); } connectionContext.subscriptions.delete(id); } }; this.onSocketConnection = async (socket, request) => { socket.upgradeReq = request; try { if (typeof socket.protocol === 'undefined' || socket.protocol !== PROTOCOL) { throw new Error('Invalid protocol'); } const connectionContext = { request, socket, subscriptions: new Map(), isConnectionInitialized: false, }; const headers = (0, utils_1.decodeHeaderFromQueryParam)(request.url); await this.options.onConnectHandler(connectionContext, headers); this.connections.add(connectionContext); const onMessage = (message) => { void this.onMessage(connectionContext, message); }; const onClose = async (error) => { socket.off('message', onMessage); socket.off('close', onClose); socket.off('error', onClose); await this.onSocketDisconnection(connectionContext, error); }; socket.on('message', onMessage); socket.on('close', onClose); socket.on('error', onClose); } catch (e) { socket.close(1002); return; } }; this.onSocketDisconnection = async (connectionContext, error) => { await this.onClose(connectionContext); if (error) { this.sendError(connectionContext, '', { message: error instanceof Error ? error.message : error }); setTimeout(() => { connectionContext.socket.close(1011); }, 10); } }; this.onMessage = (connectionContext, rawMessage) => { const message = JSON.parse(rawMessage); try { switch (message.type) { case message_types_1.MESSAGE_TYPES.GQL_CONNECTION_INIT: if ((0, message_type_guards_1.isSubscriptionConnectionInitMessage)(message)) { return this.onConnectionInit(connectionContext); } break; case message_types_1.MESSAGE_TYPES.GQL_START: if ((0, message_type_guards_1.isSubscriptionStartMessage)(message)) { return this.onSubscriptionStart(connectionContext, message); } break; case message_types_1.MESSAGE_TYPES.GQL_STOP: if ((0, message_type_guards_1.isSubscriptionStopMessage)(message)) { return this.onUnsubscribe(connectionContext, message); } } throw new Error('Invalid message'); } catch (e) { this.sendError(connectionContext, '', { errors: [{ message: e.message }] }); } return undefined; }; this.sendMessage = (connectionContext, subscriptionId, type, data) => { const message = { type, id: subscriptionId, payload: data, }; if (connectionContext.socket.readyState === WebSocket.OPEN) { connectionContext.socket.send(JSON.stringify(message)); } }; this.sendError = (connectionContext, subscriptionId, errorPayload, type = message_types_1.MESSAGE_TYPES.GQL_ERROR) => { if ([message_types_1.MESSAGE_TYPES.GQL_CONNECTION_ERROR, message_types_1.MESSAGE_TYPES.GQL_ERROR].indexOf(type) === -1) { throw new Error(`Message type should for error should be one of ${message_types_1.MESSAGE_TYPES.GQL_ERROR} or ${message_types_1.MESSAGE_TYPES.GQL_CONNECTION_ERROR}`); } this.sendMessage(connectionContext, subscriptionId, type, errorPayload); }; this.setupPing = (connectionContext) => { connectionContext.pingIntervalHandle = setInterval(() => { this.sendMessage(connectionContext, undefined, message_types_1.MESSAGE_TYPES.GQL_CONNECTION_KEEP_ALIVE, undefined); }, this.options.keepAlive); }; this.onConnectionInit = (connectionContext) => { connectionContext.isConnectionInitialized = true; const response = { type: message_types_1.MESSAGE_TYPES.GQL_CONNECTION_ACK, payload: { connectionTimeout: this.options.connectionTimeoutDuration, }, }; this.sendMessage(connectionContext, undefined, response.type, response.payload); this.setupPing(connectionContext); this.setupPing(connectionContext); }; this.onSubscriptionStart = async (connectionContext, message) => { const id = message.id; const data = JSON.parse(message.payload.data); const query = (0, graphql_1.parse)(data.query); const variables = data.variables; const headers = message.payload.extensions.authorization; if (connectionContext.subscriptions && connectionContext.subscriptions.has(id)) { await this.stopAsyncIterator(connectionContext, id); } const asyncIterator = await this.options.onSubscribeHandler(query, variables, headers, connectionContext.request); if (asyncIterator.errors) { const error = { errors: asyncIterator.errors, data: asyncIterator.data || null, }; this.sendError(connectionContext, id, error, message_types_1.MESSAGE_TYPES.GQL_ERROR); } else { const subscription = { id, asyncIterator: asyncIterator, document: query, variables, }; connectionContext.subscriptions.set(id, subscription); this.sendMessage(connectionContext, id, message_types_1.MESSAGE_TYPES.GQL_START_ACK, {}); await this.attachAsyncIterator(connectionContext, subscription); } }; this.attachAsyncIterator = async (connectionContext, sub) => { const { asyncIterator, id } = sub; let done = false; do { const { value, done: doneResult } = await asyncIterator.next(); done = doneResult; if (done) { break; } this.sendMessage(connectionContext, id, message_types_1.MESSAGE_TYPES.GQL_DATA, value); } while (!done); }; this.connections = new Set(); this.options = { ...DEFAULT_OPTIONS, ...options }; if (server) { this.attachWebServer(server); } } attachWebServer(serverOptions) { this.webSocketServer = new ws_1.Server({ ...serverOptions, path: exports.REALTIME_SUBSCRIPTION_PATH }); } start() { if (!this.webSocketServer) { throw new Error('No server is attached'); } this.webSocketServer.on('connection', this.onSocketConnection); } async stop() { var _a, _b; (_a = this.webSocketServer) === null || _a === void 0 ? void 0 : _a.off('connection', this.onSocketConnection); for (const connection of Array.from(this.connections)) { await this.onClose(connection); } (_b = this.webSocketServer) === null || _b === void 0 ? void 0 : _b.close(); } } exports.WebsocketSubscriptionServer = WebsocketSubscriptionServer; //# sourceMappingURL=server.js.map