"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const http_1 = require("http");
const graphql_1 = require("graphql");
const WebSocket = require("ws");
const subscriptions_transport_ws_1 = require("subscriptions-transport-ws");
const parseUrl = require("parseurl");
const pluginHook_1 = require("../pluginHook");
const createPostGraphileHttpRequestHandler_1 = require("./createPostGraphileHttpRequestHandler");
const liveSubscribe_1 = require("./liveSubscribe");
function lowerCaseKeys(obj) {
    return Object.keys(obj).reduce((memo, key) => {
        memo[key.toLowerCase()] = obj[key];
        return memo;
    }, {});
}
function deferred() {
    let resolve;
    let reject;
    const promise = new Promise((_resolve, _reject) => {
        resolve = _resolve;
        reject = _reject;
    });
    // tslint:disable-next-line prefer-object-spread
    return Object.assign(promise, {
        // @ts-ignore This isn't used before being defined.
        resolve,
        // @ts-ignore This isn't used before being defined.
        reject,
    });
}
async function enhanceHttpServerWithSubscriptions(websocketServer, postgraphileMiddleware, subscriptionServerOptions) {
    if (websocketServer['__postgraphileSubscriptionsEnabled']) {
        return;
    }
    websocketServer['__postgraphileSubscriptionsEnabled'] = true;
    const { options, getGraphQLSchema, withPostGraphileContextFromReqRes, handleErrors, } = postgraphileMiddleware;
    const pluginHook = pluginHook_1.pluginHookFromOptions(options);
    const externalUrlBase = options.externalUrlBase || '';
    const graphqlRoute = options.graphqlRoute || '/graphql';
    const schema = await getGraphQLSchema();
    const keepalivePromisesByContextKey = {};
    const contextKey = (ws, opId) => ws['postgraphileId'] + '|' + opId;
    const releaseContextForSocketAndOpId = (ws, opId) => {
        const promise = keepalivePromisesByContextKey[contextKey(ws, opId)];
        if (promise) {
            promise.resolve();
            keepalivePromisesByContextKey[contextKey(ws, opId)] = null;
        }
    };
    const addContextForSocketAndOpId = (context, ws, opId) => {
        releaseContextForSocketAndOpId(ws, opId);
        const promise = deferred();
        promise['context'] = context;
        keepalivePromisesByContextKey[contextKey(ws, opId)] = promise;
        return promise;
    };
    const applyMiddleware = async (middlewares = [], req, res) => {
        for (const middleware of middlewares) {
            // TODO: add Koa support
            await new Promise((resolve, reject) => {
                middleware(req, res, err => (err ? reject(err) : resolve()));
            });
        }
    };
    const reqResFromSocket = async (socket) => {
        const req = socket['__postgraphileReq'];
        if (!req) {
            throw new Error('req could not be extracted');
        }
        let dummyRes = socket['__postgraphileRes'];
        if (req.res) {
            throw new Error("Please get in touch with Benjie; we weren't expecting req.res to be present but we want to reserve it for future usage.");
        }
        if (!dummyRes) {
            dummyRes = new http_1.ServerResponse(req);
            dummyRes.writeHead = (statusCode, _statusMessage, headers) => {
                if (statusCode && statusCode > 200) {
                    // tslint:disable-next-line no-console
                    console.error(`Something used 'writeHead' to write a '${statusCode}' error for websockets - check the middleware you're passing!`);
                    socket.close();
                }
                else if (headers) {
                    // tslint:disable-next-line no-console
                    console.error("Passing headers to 'writeHead' is not supported with websockets currently - check the middleware you're passing");
                    socket.close();
                }
            };
            await applyMiddleware(options.websocketMiddlewares, req, dummyRes);
            // reqResFromSocket is only called once per socket, so there's no race condition here
            // eslint-disable-next-line require-atomic-updates
            socket['__postgraphileRes'] = dummyRes;
        }
        return { req, res: dummyRes };
    };
    const getContext = (socket, opId) => {
        return new Promise((resolve, reject) => {
            reqResFromSocket(socket)
                .then(({ req, res }) => withPostGraphileContextFromReqRes(req, res, { singleStatement: true }, context => {
                const promise = addContextForSocketAndOpId(context, socket, opId);
                resolve(promise['context']);
                return promise;
            }))
                .then(null, reject);
        });
    };
    const wss = new WebSocket.Server({ noServer: true });
    let socketId = 0;
    websocketServer.on('upgrade', (req, socket, head) => {
        const { pathname = '' } = parseUrl(req) || {};
        const isGraphqlRoute = pathname === externalUrlBase + graphqlRoute;
        if (isGraphqlRoute) {
            wss.handleUpgrade(req, socket, head, ws => {
                wss.emit('connection', ws, req);
            });
        }
    });
    const staticValidationRules = pluginHook('postgraphile:validationRules:static', graphql_1.specifiedRules, {
        options,
    });
    subscriptions_transport_ws_1.SubscriptionServer.create({
        schema,
        validationRules: staticValidationRules,
        execute: () => {
            throw new Error('Only subscriptions are allowed over websocket transport');
        },
        subscribe: options.live ? liveSubscribe_1.default : graphql_1.subscribe,
        onConnect(connectionParams, _socket, connectionContext) {
            const { socket, request } = connectionContext;
            socket['postgraphileId'] = ++socketId;
            if (!request) {
                throw new Error('No request!');
            }
            const normalizedConnectionParams = lowerCaseKeys(connectionParams);
            request['connectionParams'] = connectionParams;
            request['normalizedConnectionParams'] = normalizedConnectionParams;
            socket['__postgraphileReq'] = request;
            if (!request.headers.authorization && normalizedConnectionParams['authorization']) {
                /*
                 * Enable JWT support through connectionParams.
                 *
                 * For other headers you'll need to do this yourself for security
                 * reasons (e.g. we don't want to allow overriding of Origin /
                 * Referer / etc)
                 */
                request.headers.authorization = String(normalizedConnectionParams['authorization']);
            }
            socket['postgraphileHeaders'] = {
                ...normalizedConnectionParams,
                // The original headers must win (for security)
                ...request.headers,
            };
        },
        // tslint:disable-next-line no-any
        async onOperation(message, params, socket) {
            const opId = message.id;
            const context = await getContext(socket, opId);
            // Override schema (for --watch)
            params.schema = await getGraphQLSchema();
            Object.assign(params.context, context);
            const { req, res } = await reqResFromSocket(socket);
            const meta = {};
            const formatResponse = (response) => {
                if (response.errors) {
                    response.errors = handleErrors(response.errors, req, res);
                }
                if (!createPostGraphileHttpRequestHandler_1.isEmpty(meta)) {
                    response['meta'] = meta;
                }
                return response;
            };
            // onOperation is only called once per params object, so there's no race condition here
            // eslint-disable-next-line require-atomic-updates
            params.formatResponse = formatResponse;
            const hookedParams = pluginHook
                ? pluginHook('postgraphile:ws:onOperation', params, {
                    message,
                    params,
                    socket,
                    options,
                })
                : params;
            const finalParams = {
                ...hookedParams,
                query: typeof hookedParams.query !== 'string' ? hookedParams.query : graphql_1.parse(hookedParams.query),
            };
            // You are strongly encouraged to use
            // `postgraphile:validationRules:static` if possible - you should
            // only use this one if you need access to variables.
            const moreValidationRules = pluginHook('postgraphile:validationRules', [], {
                options,
                req,
                res,
                variables: params.variables,
                operationName: params.operationName,
                meta,
            });
            if (moreValidationRules.length) {
                const validationErrors = graphql_1.validate(params.schema, finalParams.query, moreValidationRules);
                if (validationErrors.length) {
                    const error = new Error('Query validation failed: \n' + validationErrors.map(e => e.message).join('\n'));
                    error['errors'] = validationErrors;
                    return Promise.reject(error);
                }
            }
            return finalParams;
        },
        onOperationComplete(socket, opId) {
            releaseContextForSocketAndOpId(socket, opId);
        },
        /*
         * Heroku times out after 55s:
         *   https://devcenter.heroku.com/articles/error-codes#h15-idle-connection
         *
         * The subscriptions-transport-ws client times out by default 30s after last keepalive:
         *   https://github.com/apollographql/subscriptions-transport-ws/blob/52758bfba6190169a28078ecbafd2e457a2ff7a8/src/defaults.ts#L1
         *
         * GraphQL Playground times out after 20s:
         *   https://github.com/prisma/graphql-playground/blob/fa91e1b6d0488e6b5563d8b472682fe728ee0431/packages/graphql-playground-react/src/state/sessions/fetchingSagas.ts#L81
         *
         * Pick a number under these ceilings.
         */
        keepAlive: 15000,
        ...subscriptionServerOptions,
    }, wss);
}
exports.enhanceHttpServerWithSubscriptions = enhanceHttpServerWithSubscriptions;
//# sourceMappingURL=data:application/json;base64, 