"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Client = void 0;
const uuid_1 = require("uuid");
const retimer_1 = __importDefault(require("retimer"));
const steed_1 = __importDefault(require("steed"));
class Client {
    constructor(connection, server) {
        this.connection = connection;
        this.server = server;
        this.subscriptions = {};
        this.nextId = 1;
        this.inflight = {};
        this.inflightCounter = 0;
        this.keepAlive = 0;
        this._lastDedupId = -1;
        this._closed = false;
        this._closing = false;
        this.logger = server.logger;
        this.setup();
    }
    setup() {
        const connection = this.connection;
        connection.on('error', () => { });
        const completeConnection = () => {
            this.setupTimer();
            connection.connack({
                returnCode: 0,
                sessionPresent: false,
            });
            this.logger.info('client connected');
            this.server.emit('clientConnected', this);
            connection.on('puback', packet => {
                this.setupTimer();
                this.handlePubAck(packet);
            });
            connection.on('pingreq', () => {
                this.logger.debug('pingreq');
                this.setupTimer();
                this.handlePingReq();
                this.connection.pingresp();
            });
            connection.on('subscribe', packet => {
                this.setupTimer();
                this.handleSubscribe(packet);
            });
            connection.on('publish', packet => {
                this.setupTimer();
                this.handlePublish(packet);
            });
            connection.on('unsubscribe', packet => {
                this.setupTimer();
                this.logger.info({ packet }, 'unsubscribe received');
                (0, steed_1.default)().map(this, packet.unsubscriptions, this.unsubscribeMapTo, err => {
                    if (err) {
                        this.logger.warn(err);
                        this.close(null, err.message);
                        return;
                    }
                    connection.unsuback({
                        messageId: packet.messageId,
                    });
                });
            });
            connection.on('disconnect', () => {
                this.logger.debug('disconnect requested');
                this.close(null, 'disconnect request');
            });
            connection.on('error', this.handleError.bind(this));
            connection.removeListener('error', () => { });
            connection.on('close', () => {
                this.onNonDisconnectClose('close');
            });
        };
        connection.once('connect', packet => {
            this.handleConnect(packet, completeConnection);
        });
    }
    handleError(err) {
        this.server.emit('clientError', err, this);
        this.onNonDisconnectClose(err.message);
    }
    setupTimer() {
        if (this.keepAlive <= 0) {
            return;
        }
        const timeout = (this.keepAlive * 1000 * 3) / 2;
        this.logger.debug({ timeout: timeout }, 'setting keepAlive timeout');
        if (this.timer) {
            this.timer.reschedule(timeout);
        }
        else {
            this.timer = (0, retimer_1.default)(() => {
                this.logger.info('keepAlive timeout');
                this.onNonDisconnectClose('keepAlive timeout');
            }, timeout);
        }
    }
    doForward(err, packet) {
        if (err) {
            return this.connection && this.connection.emit('error', err);
        }
        this.connection.publish(packet);
        if (packet.qos === 1) {
            this.inflight[packet.messageId] = packet;
        }
    }
    forward(topic, payload, options, subTopic, qos, cb) {
        if (options._dedupId <= this._lastDedupId) {
            return;
        }
        this.logger.trace({ topic: topic }, 'delivering message');
        const indexWildcard = subTopic.indexOf('#');
        const indexPlus = subTopic.indexOf('+');
        let forward = true;
        const newId = this.nextId++;
        this.nextId %= 65536;
        const packet = {
            topic,
            payload,
            qos,
            messageId: newId,
        };
        if (qos) {
            this.inflightCounter++;
        }
        if (this._closed || this._closing) {
            this.logger.debug({ packet: packet }, 'trying to send a packet to a disconnected client');
            forward = false;
        }
        else if (this.inflightCounter >= this.server.options.maxInflightMessages) {
            this.logger.warn('too many inflight packets, closing');
            this.close(null, 'too many inflight packets');
            forward = false;
        }
        if (cb) {
            cb();
        }
        forward = forward && !(topic.indexOf('$SYS') >= 0 && ((indexWildcard >= 0 && indexWildcard < 2) || (indexPlus >= 0 && indexPlus < 2)));
        if (forward) {
            if (options._dedupId === undefined) {
                options._dedupId = this.server.nextDedupId();
                this._lastDedupId = options._dedupId;
            }
            if (qos && options.messageId) {
                this.server.updateOfflinePacket(this, options.messageId, packet, this.doForward);
            }
            else {
                this.doForward(null, packet);
            }
        }
    }
    unsubscribeMapTo(topic, cb) {
        const sub = this.subscriptions[topic];
        if (!sub || !sub.handler) {
            this.server.emit('unsubscribed', topic, this);
            return cb();
        }
        this.server.listener.unsubscribe(topic, sub.handler, err => {
            if (err) {
                cb(err);
                return;
            }
            if (!this._closing || this.clean) {
                delete this.subscriptions[topic];
                this.logger.info({ topic: topic }, 'unsubscribed');
                this.server.emit('unsubscribed', topic, this);
            }
            cb();
        });
    }
    handleConnect(packet, completeConnection) {
        const client = this.connection;
        this.id = packet.clientId;
        this.logger = this.logger.child({ client: this });
        if (!this.id) {
            if (packet.protocolVersion == 4 && packet.clean) {
                this.id = (0, uuid_1.v4)();
            }
            else {
                this.logger.info('identifier rejected');
                client.connack({
                    returnCode: 2,
                });
                client.stream.end();
                return;
            }
        }
        this.keepAlive = packet.keepalive;
        this.will = packet.will;
        this.clean = packet.clean;
        if (this.id in this.server.clients) {
            this.server.clients[this.id].close(completeConnection, 'new connection request');
        }
        else {
            completeConnection();
        }
    }
    handlePingReq() {
        this.server.emit('pingReq', this);
    }
    handlePubAck(packet) {
        const logger = this.logger;
        logger.debug({ packet: packet }, 'pubAck');
        if (this.inflight[packet.messageId]) {
            this.server.emit('delivered', this.inflight[packet.messageId], this);
            this.inflightCounter--;
            delete this.inflight[packet.messageId];
        }
        else {
            logger.info({ packet: packet }, 'no matching packet');
        }
    }
    doSubscribe(s, cb) {
        const handler = (topic, payload, options) => {
            this.forward(topic, payload, options, s.topic, s.qos);
        };
        if (this.subscriptions[s.topic] === undefined) {
            this.subscriptions[s.topic] = { qos: s.qos, handler: handler };
            this.server.listener.subscribe(s.topic, handler, err => {
                if (err) {
                    delete this.subscriptions[s.topic];
                    cb(err);
                    return;
                }
                this.logger.info({ topic: s.topic, qos: s.qos }, 'subscribed to topic');
                this.subscriptions[s.topic] = { qos: s.qos, handler: handler };
                cb(null, true);
            });
        }
        else {
            cb(null, true);
        }
    }
    handleEachSub(s, cb) {
        if (this.subscriptions[s.topic] === undefined) {
            this.doSubscribe(s, cb);
        }
        else {
            cb(null, true);
        }
    }
    handleSubscribe(packet) {
        const logger = this.logger;
        logger.debug({ packet: packet }, 'subscribe received');
        const granted = Client.calculateGranted(this, packet);
        (0, steed_1.default)().map(this, packet.subscriptions, this.handleEachSub, (err, authorized) => {
            if (err) {
                this.close(null, err.message);
                return;
            }
            packet.subscriptions.forEach((sub, index) => {
                if (authorized[index]) {
                    this.server.emit('subscribed', sub.topic, this);
                }
                else {
                    granted[index] = 0x80;
                }
            });
            if (!this._closed) {
                this.connection.suback({
                    messageId: packet.messageId,
                    granted: granted,
                });
            }
        });
    }
    handlePublish(packet) {
        if (packet.qos === 2) {
            switch (this.server.onQoS2publish) {
                case 'dropToQoS1':
                    packet.qos = 1;
                    break;
                case 'disconnect':
                    if (!this._closed && !this._closing) {
                        this.close(null, 'qos2 caused disconnect');
                    }
                    return;
                default:
                    break;
            }
        }
        const doPubAck = () => {
            if (packet.qos === 1 && !(this._closed || this._closing)) {
                this.connection.puback({
                    messageId: packet.messageId,
                });
            }
        };
        doPubAck();
    }
    onNonDisconnectClose(reason) {
        const logger = this.logger;
        const will = this.will;
        if (this._closed || this._closing) {
            return;
        }
        if (this.will) {
            logger.info({ packet: will }, 'delivering last will');
            setImmediate(() => {
                this.handlePublish(will);
            });
        }
        this.close(null, reason);
    }
    close(callback, reason) {
        callback = callback || (() => { });
        if (this._closed || this._closing) {
            return callback();
        }
        if (this.id) {
            this.logger.debug('closing client, reason: ' + reason);
            if (this.timer) {
                this.timer.clear();
            }
        }
        const cleanup = () => {
            this._closed = true;
            this.logger.info('closed');
            this.connection.removeAllListeners();
            this.connection.on('error', function () { });
            this.server.emit('clientDisconnected', this, reason);
            callback();
        };
        this._closing = true;
        steed_1.default.map(this, Object.keys(this.subscriptions), this.unsubscribeMapTo, err => {
            if (err) {
                this.logger.info(err);
            }
            if (!this._closed) {
                cleanup();
                if (this.connection.stream.destroySoon) {
                    this.connection.stream.destroySoon();
                }
                else if (this.connection.stream.destroy) {
                    this.connection.stream.destroy();
                }
                else {
                    this.connection.stream.end();
                }
            }
        });
    }
    static calculateGranted(client, packet) {
        return packet.subscriptions.map(function (e) {
            if (e.qos === 2) {
                e.qos = 1;
            }
            if (client.subscriptions[e.topic] !== undefined) {
                client.subscriptions[e.topic].qos = e.qos;
            }
            return e.qos;
        });
    }
}
exports.Client = Client;
//# sourceMappingURL=client.js.map