"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (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 = 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");
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 = (connectionContext) => {
            connectionContext.subscriptions.forEach(subscriptionId => {
                this.stopAsyncIterator(connectionContext, subscriptionId.id);
            });
            if (connectionContext.pingIntervalHandle) {
                clearInterval(connectionContext.pingIntervalHandle);
                connectionContext.pingIntervalHandle = null;
            }
            this.connections.delete(connectionContext);
        };
        this.onUnsubscribe = (connectionContext, messageOrSubscriptionId) => {
            const { id } = messageOrSubscriptionId;
            this.stopAsyncIterator(connectionContext, id);
            this.sendMessage(connectionContext, id, message_types_1.MESSAGE_TYPES.GQL_COMPLETE, {});
        };
        this.stopAsyncIterator = (connectionContext, id) => {
            if (connectionContext.subscriptions && connectionContext.subscriptions.has(id)) {
                const subscription = connectionContext.subscriptions.get(id);
                if (subscription.asyncIterator) {
                    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 => {
                    this.onMessage(connectionContext, message);
                };
                const onClose = (error) => {
                    socket.off('message', onMessage);
                    socket.off('close', onClose);
                    socket.off('error', onClose);
                    this.onSocketDisconnection(connectionContext, error);
                };
                socket.on('message', onMessage);
                socket.on('close', onClose);
                socket.on('error', onClose);
            }
            catch (e) {
                console.log(e);
                socket.close(1002);
                return;
            }
        };
        this.onSocketDisconnection = (connectionContext, error) => {
            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, message);
                        }
                        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 }] });
            }
        };
        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, message) => {
            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)) {
                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, {});
                this.attachAsyncIterator(connectionContext, subscription);
            }
        };
        this.attachAsyncIterator = async (connectionContext, sub) => {
            const { asyncIterator, id } = sub;
            while (true) {
                const { value, done } = await asyncIterator.next();
                if (done) {
                    break;
                }
                this.sendMessage(connectionContext, id, message_types_1.MESSAGE_TYPES.GQL_DATA, value);
            }
        };
        this.connections = new Set();
        this.options = { ...DEFAULT_OPTIONS, ...options };
        if (server) {
            this.attachWebServer(server);
        }
    }
    attachWebServer(serverOptions) {
        this.webSocketServer = new ws_1.Server(serverOptions || {});
    }
    start() {
        if (!this.webSocketServer) {
            throw new Error('No server is attached');
        }
        this.webSocketServer.on('connection', this.onSocketConnection);
    }
    stop() {
        if (this.webSocketServer) {
            this.webSocketServer.off('connection', this.onSocketConnection);
            this.connections.forEach(connection => {
                this.onClose(connection);
            });
            this.webSocketServer.close();
        }
    }
}
exports.WebsocketSubscriptionServer = WebsocketSubscriptionServer;
//# sourceMappingURL=server.js.map