"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SubscriptionServer = void 0;
const chalk_1 = __importDefault(require("chalk"));
const crypto_1 = __importDefault(require("crypto"));
const promise_toolbox_1 = require("promise-toolbox");
const http_1 = require("http");
const ip_1 = require("ip");
const util_1 = require("util");
const mqtt_server_1 = require("./subscription/mqtt-server");
const get_port_1 = __importDefault(require("get-port"));
const MINUTE = 1000 * 60;
const CONNECTION_TIMEOUT = 2 * MINUTE;
const TOPIC_EXPIRATION_TIMEOUT = 60 * MINUTE;
const BASE_PORT = 8900;
const MAX_PORT = 9999;
const log = console;
class SubscriptionServer {
    constructor(config, appSyncServerContext) {
        this.config = config;
        this.appSyncServerContext = appSyncServerContext;
        this.port = config.wsPort;
        this.mqttWebSocketServer = (0, http_1.createServer)();
        this.mqttServer = new mqtt_server_1.MQTTServer({
            logger: {
                level: process.env.DEBUG ? 'debug' : 'error',
            },
        });
        this.mqttServer.attachHttpServer(this.mqttWebSocketServer);
        this.clientRegistry = new Map();
        this.mqttIteratorTimeout = new Map();
        this.mqttServer.on('clientConnected', this.afterMQTTClientConnect.bind(this));
        this.mqttServer.on('clientDisconnected', this.afterMQTTClientDisconnect.bind(this));
        this.mqttServer.on('subscribed', this.afterSubscription.bind(this));
        this.mqttServer.on('unsubscribed', this.afterMQTTClientUnsubscribe.bind(this));
        this.realtimeSocketServer = (0, http_1.createServer)();
    }
    async start() {
        if (!this.port) {
            this.port = await (0, get_port_1.default)({
                port: get_port_1.default.makeRange(BASE_PORT, MAX_PORT),
            });
        }
        else {
            try {
                await (0, get_port_1.default)({
                    port: this.port,
                });
            }
            catch (e) {
                throw new Error(`Port ${this.port} is already in use. Please kill the program using this port and restart Mock`);
            }
        }
        const server = this.mqttWebSocketServer.listen(this.port);
        return await (0, promise_toolbox_1.fromEvent)(server, 'listening').then(() => {
            const address = server.address();
            this.url = `ws://${(0, ip_1.address)()}:${address.port}/`;
            return server;
        });
    }
    stop() {
        if (this.mqttWebSocketServer) {
            this.mqttWebSocketServer.close();
            this.url = null;
            this.mqttWebSocketServer = null;
        }
    }
    async afterMQTTClientConnect(client) {
        const { id: clientId } = client;
        log.info(`Client (${chalk_1.default.bold(clientId)}) connected to subscription server`);
        const timeout = this.mqttIteratorTimeout.get(client.id);
        if (timeout) {
            clearTimeout(timeout);
        }
    }
    async afterSubscription(topic, client) {
        const { id: clientId } = client;
        log.info(`Client (${chalk_1.default.bold(clientId)}) subscribed to topic ${topic}`);
        const regs = this.clientRegistry.get(clientId);
        if (!regs) {
            log.error(`No registration for client (${chalk_1.default.bold(clientId)})`);
            return;
        }
        const reg = regs.find(({ topicId }) => topicId === topic);
        if (!reg) {
            log.error(`Client (${chalk_1.default.bold(clientId)}) tried to subscribe to non-existent topic ${topic}`);
            return;
        }
        const { asyncIterator, topicId } = reg;
        if (!reg.isRegistered) {
            this.register(reg.document, reg.variables, reg.context, asyncIterator);
        }
        while (true) {
            let { value: payload } = await asyncIterator.next();
            log.info(`Publishing payload for topic ${topicId}`);
            log.log('Payload:');
            log.log((0, util_1.inspect)(payload));
            this.mqttServer.publish({
                topic: topicId,
                payload: JSON.stringify(payload),
                qos: 0,
                retain: false,
            });
        }
    }
    afterMQTTClientUnsubscribe(topic, client) {
        const { id: clientId } = client;
        log.info(`Client (${chalk_1.default.bold(clientId)}) unsubscribed from topic ${topic}`);
        const registration = this.clientRegistry.get(clientId);
        if (!registration) {
            log.error(`No registration for client (${chalk_1.default.bold(clientId)})`);
            return;
        }
        const reg = registration.find(({ topicId }) => topicId === topic);
        if (!reg) {
            log.error(`Client (${chalk_1.default.bold(clientId)}) tried to unsubscribe from non-existent topic ${topic}`);
            return;
        }
        reg.asyncIterator.return();
        reg.isRegistered = false;
    }
    afterMQTTClientDisconnect(client) {
        const { id: clientId } = client;
        log.info(`Client (${chalk_1.default.bold(clientId)}) disconnected`);
        const reg = this.clientRegistry.get(clientId);
        if (!reg) {
            log.error(`Unregistered client (${chalk_1.default.bold(clientId)}) disconnected`);
        }
        reg.forEach(subscription => {
            subscription.asyncIterator.return();
        });
        this.clientRegistry.delete(clientId);
    }
    async register(document, variables, context, asyncIterator) {
        const connection = context.request.connection;
        const remoteAddress = `${connection.remoteAddress}:${connection.remotePort}`;
        const clientId = crypto_1.default.createHash('MD5').update(remoteAddress).digest().toString('hex');
        const subscriptionName = document.definitions[0].selectionSet.selections.find(s => s.kind === 'Field').name.value;
        const paramHash = variables && Object.keys(variables).length
            ? crypto_1.default.createHash('MD5').update(JSON.stringify(variables)).digest().toString('hex')
            : null;
        const topicId = [clientId, subscriptionName, paramHash].join('/');
        log.info(`Client (${chalk_1.default.bold(clientId)}) registered for topic ${topicId}`);
        const registration = {
            context,
            document,
            variables,
            topicId,
            asyncIterator: asyncIterator,
            isRegistered: true,
        };
        const currentRegistrations = this.clientRegistry.get(clientId) || [];
        const existingSubscription = currentRegistrations.find(reg => reg.topicId === topicId);
        if (!existingSubscription) {
            currentRegistrations.push(registration);
            this.clientRegistry.set(clientId, currentRegistrations);
            this.mqttIteratorTimeout.set(clientId, setTimeout(() => {
                asyncIterator.return();
                this.mqttIteratorTimeout.delete(clientId);
            }, CONNECTION_TIMEOUT));
        }
        else {
            Object.assign(existingSubscription, registration);
        }
        return {
            extensions: {
                subscription: {
                    mqttConnections: [
                        {
                            url: this.url,
                            topics: currentRegistrations.map(reg => reg.topicId),
                            client: clientId,
                        },
                    ],
                    newSubscriptions: {
                        [subscriptionName]: {
                            topic: topicId,
                            expireTime: Date.now() + TOPIC_EXPIRATION_TIMEOUT,
                        },
                    },
                },
            },
        };
    }
}
exports.SubscriptionServer = SubscriptionServer;
//# sourceMappingURL=subscription.js.map