"use strict";
var NatsClient_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.NatsClient = exports.NatsClientSettings = void 0;
const tslib_1 = require("tslib");
const inversify_1 = require("inversify");
const _ = require("lodash");
const guid_typescript_1 = require("guid-typescript");
const nats = require("nats");
const YAML = require("yaml");
const codiac_common_1 = require("@codiac.io/codiac-common");
const contracts_1 = require("@codiac.io/codiac-common/contracts");
const i_messaging_client_1 = require("../../uilogic/i-messaging-client");
const codiac_api_1 = require("../codiac-api");
let NatsClientSettings = class NatsClientSettings {
    constructor(natsHost, natsPort, serializer, 
    /** The amount of time to wait for a response to a given request.  */
    requestTimeoutInSecs) {
        this.natsHost = natsHost;
        this.natsPort = natsPort;
        this.serializer = serializer;
        this.requestTimeoutInSecs = requestTimeoutInSecs;
    }
};
NatsClientSettings = tslib_1.__decorate([
    (0, inversify_1.injectable)(),
    tslib_1.__metadata("design:paramtypes", [String, Number, String, Number])
], NatsClientSettings);
exports.NatsClientSettings = NatsClientSettings;
let NatsClient = NatsClient_1 = class NatsClient {
    static bootstrap(ioc, settings) {
        ioc.bind(NatsClientSettings).toConstantValue(settings);
        ioc.bind(NatsClient_1).toSelf();
    }
    constructor(apiUserCreds, settings, logger) {
        this.apiUserCreds = apiUserCreds;
        this.logger = logger;
        this.listeners = [];
        this.settings = _.merge({}, NatsClient_1.defaults, settings !== null && settings !== void 0 ? settings : {}); // Overlays the provided settings over the defaults
        this.natsUrl = "nats://".concat(this.settings.natsHost, ":", this.settings.natsPort.toString());
    }
    connect() {
        var _a;
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            try {
                this.cnxn = yield nats.connect({
                    servers: [this.natsUrl]
                });
                return this.connectionId;
            }
            catch (err) {
                const errMsg = "Error connecting to ".concat("[", this.natsUrl, "]: ", (_a = err.message) !== null && _a !== void 0 ? _a : "no details available");
                this.logger.error(errMsg, err.stack);
                throw new Error(errMsg);
            }
        });
    }
    disconnect() {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            try {
                if (this.cnxn && (!this.cnxn.isClosed())) {
                    this.logger.detail(`Closing connection`);
                    yield this.cnxn.drain();
                    yield this.cnxn.close();
                }
                else {
                    this.logger.detail(`Skipping disconnect: No connection is currently open.`);
                }
                return;
            }
            catch (err) {
                throw err;
            }
        });
    }
    isConnected() {
        return !(this.cnxn == undefined || this.cnxn.isClosed());
    }
    get connectionId() {
        var _a, _b;
        return (_b = (_a = this.cnxn) === null || _a === void 0 ? void 0 : _a.info) === null || _b === void 0 ? void 0 : _b.client_id.toString();
    }
    /** TODO: Expose typesafe messages instead of raw nats.Msg */
    subscribe(topics, handler) {
        var _a, _b, _c;
        // if (this.cnxn == undefined || this.cnxn.isClosed()) {
        //   await this.connect();
        // }
        // if (this.cnxn == undefined) throw new Error(`Unable to subscribe: Failed connecting to nats server.`);
        try {
            if (!this.isConnected() || this.cnxn == undefined) {
                throw new Error("Client is not connected to the nats server.");
            }
            let optz = {
                callback: (errResponse, natsMsg) => tslib_1.__awaiter(this, void 0, void 0, function* () {
                    var _d, _e, _f, _g, _h, _j, _k;
                    try {
                        this.logger.debug("received message ".concat(natsMsg.sid.toString(), " ", natsMsg.subject));
                        // let asString = natsMsg.string();
                        // EXTRACT META OBJECT
                        let meta = {
                            sid: natsMsg.sid,
                            subject: natsMsg.subject,
                            action: (_e = (_d = natsMsg.headers) === null || _d === void 0 ? void 0 : _d.get("action")) !== null && _e !== void 0 ? _e : i_messaging_client_1.MessagingDefaults.ACTION,
                            correlationId: (_g = (_f = natsMsg.headers) === null || _f === void 0 ? void 0 : _f.get("correlationId")) !== null && _g !== void 0 ? _g : guid_typescript_1.Guid.raw(),
                        };
                        if (natsMsg.headers) {
                            let dict = {};
                            // filter to prevent overwriting any IMessageReceivedMeta properties 
                            // This will forfeit any colliding custom headers 
                            // TO allow for that, consider isolating custom meta with a separate "custom" property.
                            natsMsg.headers.keys().filter(k => !Object.keys(meta).includes(k)).forEach(k => {
                                var _a;
                                const val = (_a = natsMsg.headers) === null || _a === void 0 ? void 0 : _a.get(k);
                                if (val != undefined)
                                    dict[k] = val;
                            });
                            meta = _.merge(meta, dict);
                        }
                        if (natsMsg.reply)
                            meta.replyOn = natsMsg.reply;
                        // DESERIALIZE
                        // TODO: Consider a tryParse/else to support messages as native types (string|number|boolean) 
                        let msg;
                        try {
                            msg = this.deserialize(natsMsg);
                        }
                        catch (err) {
                            this.logger.error(err.message);
                            if (errResponse == undefined) {
                                errResponse = err;
                            }
                            else {
                                // This isnt a good spot for this error, 
                                // but if nats is already returning a transmission error, 
                                // then its very possible that a malformed message is related to the problem.
                                errResponse.chainedError = err;
                            }
                            msg = natsMsg.string(); // sent OUT just like it was sent IN 
                        }
                        // FIRE THE PROVIDED HANDLER...
                        yield handler(errResponse, msg, meta, natsMsg);
                    }
                    catch (err) {
                        this.logger.error("Error handling message SID:".concat(natsMsg.sid.toString(), ", on ", topics, ": ", (_h = err.name) !== null && _h !== void 0 ? _h : "", (_k = (_j = err.message) !== null && _j !== void 0 ? _j : err.stack) !== null && _k !== void 0 ? _k : ""));
                    }
                    finally {
                        return;
                    }
                })
            }; // <--- These will likely need to be exposed very soon
            let subx = this.cnxn.subscribe(topics, optz);
            this.listeners.push(subx);
            const subxId = subx.getID().toString();
            this.logger.notice("Successfully subscribed to: ".concat("[", topics, "]", ", [subxId: ", subxId, "]"));
            return subxId;
        }
        catch (err) {
            let errMsg = "Error subscribing: ".concat((err.name != "Error") ? (_a = err.name) !== null && _a !== void 0 ? _a : "" : "", " ", (_c = (_b = err.message) !== null && _b !== void 0 ? _b : err.stack) !== null && _c !== void 0 ? _c : "");
            this.logger.error(errMsg, err.stack);
            throw new Error(errMsg);
        }
    }
    /** Stops listening on the given topics. */
    unsubscribe(topics, options) {
        var _a, _b, _c;
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            try {
                let subx = this.listeners.find(sx => sx.getSubject() === topics);
                if (subx != undefined) {
                    if ((options === null || options === void 0 ? void 0 : options.force) != true) {
                        yield (subx === null || subx === void 0 ? void 0 : subx.drain());
                    }
                    subx.unsubscribe();
                    this.logger.notice("Successfully unsubscribed from: ".concat("[", topics, "]"));
                    return true;
                }
                else {
                    this.logger.warn("Unable to unsubscribe, no subscription found for: ".concat("[", topics, "]"));
                    return false;
                }
            }
            catch (err) {
                this.logger.error("Error unsubscribing: ".concat((_a = err.name) !== null && _a !== void 0 ? _a : "", (_c = (_b = err.message) !== null && _b !== void 0 ? _b : err.stack) !== null && _c !== void 0 ? _c : ""));
                return false;
            }
        });
    }
    /** THrows any errors. */
    deserialize(raw) {
        var _a, _b;
        let output;
        try {
            if (this.settings.serializer === "string") {
                output = raw.string();
            }
            else {
                let asString = raw.string();
                output = (this.settings.serializer == 'yaml') ? YAML.parse(asString) : JSON.parse(asString);
            }
            return output;
        }
        catch (err) {
            let msg = "Error deserializing message on ".concat(raw.subject, " to ", this.settings.serializer, ": ", (_b = (_a = err.message) !== null && _a !== void 0 ? _a : err.stack) !== null && _b !== void 0 ? _b : "");
            this.logger.error(msg);
            // output = raw.string() as T; 
            throw new Error(msg);
        }
    }
    serialize(msg) {
        var _a, _b;
        try {
            let payload;
            if (typeof msg === 'string') {
                payload = msg; // already serialized
            }
            else {
                // TODO: support a dataype or serializer override specified in the meta
                payload = (this.settings.serializer == "yaml") ? YAML.stringify(msg) : JSON.stringify(msg);
            }
            // Commented out bc we're only transmitting within the local machine, 
            // so serializing to int array is just adding processing time,
            // without any improvement to latency or security. 
            // return Buffer.from(payload, 'utf8');
            return payload;
        }
        catch (err) {
            let msg = "Error serializing message to ".concat(this.settings.serializer, ": ", (_b = (_a = err.message) !== null && _a !== void 0 ? _a : err.stack) !== null && _b !== void 0 ? _b : "");
            this.logger.error(msg, err.stack);
            throw new Error(msg);
        }
    }
    // public async publish<TMsg>(topic: string, msg: TMsg, headers?: { [name: string]: string }, replyOn?: string): Promise<void> {
    publish(msg, meta) {
        var _a, _b, _c, _d, _e;
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            if (this.cnxn == undefined || this.cnxn.isClosed()) {
                yield this.connect();
            }
            if (this.cnxn == undefined)
                throw new Error(`Unable to publish message: Failed connecting to nats server.`);
            try {
                const subject = (_a = meta === null || meta === void 0 ? void 0 : meta.subject) !== null && _a !== void 0 ? _a : i_messaging_client_1.MessagingDefaults.SUBJECT;
                const payload = this.serialize(msg);
                let optz = {}; // <--- These will likely need to be exposed very soon
                var ultimateMeta = (meta != undefined) ? _.omit(meta, "subject", "replyOn") : {};
                if (ultimateMeta.action == undefined)
                    ultimateMeta.action = i_messaging_client_1.MessagingDefaults.ACTION;
                if (ultimateMeta.correlationId == undefined)
                    ultimateMeta.correlationId = guid_typescript_1.Guid.raw();
                optz.headers = this.convertToNatsHeaders(ultimateMeta);
                // Add auth token header if not already provided. 
                if (((_b = this.apiUserCreds) === null || _b === void 0 ? void 0 : _b.apiToken) && !optz.headers.has(NatsClient_1.DEFAULT_TOKEN_HEADER_PARAM)) {
                    optz.headers.set(NatsClient_1.DEFAULT_TOKEN_HEADER_PARAM, this.apiUserCreds.apiToken);
                }
                if (meta === null || meta === void 0 ? void 0 : meta.replyOn)
                    optz.reply = meta.replyOn;
                // SET IT AND FORGET IT...
                // NOTE: to get a response, use "request()" instead of "publish()"
                this.cnxn.publish(subject, payload, optz);
            }
            catch (err) {
                this.logger.error("Error publishing: ".concat((_c = err.name) !== null && _c !== void 0 ? _c : "", (_e = (_d = err.message) !== null && _d !== void 0 ? _d : err.stack) !== null && _e !== void 0 ? _e : ""), meta);
            }
            finally {
                return;
            }
        });
    }
    convertToNatsHeaders(meta) {
        let headers = new nats.MsgHdrsImpl(); // <-- is this what they want us doing here?
        if (meta) {
            Object.keys(meta).forEach(k => {
                headers.append(k, meta[k]);
            });
        }
        return headers;
    }
    request(msg, meta, timeoutInSecs) {
        var _a, _b, _c, _d, _e, _f;
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            let result = new contracts_1.Result(false);
            try {
                if (this.cnxn == undefined || this.cnxn.isClosed()) {
                    yield this.connect();
                }
                if (this.cnxn == undefined)
                    throw new Error(`Unable to perform request: Failed connecting to nats server.`);
                const subject = (_a = meta === null || meta === void 0 ? void 0 : meta.subject) !== null && _a !== void 0 ? _a : i_messaging_client_1.MessagingDefaults.SUBJECT;
                const payload = this.serialize(msg);
                let optz = {
                    timeout: (timeoutInSecs !== null && timeoutInSecs !== void 0 ? timeoutInSecs : this.settings.requestTimeoutInSecs) * 1000
                }; // <--- These will likely need to be exposed very soon
                var ultimateMeta = (meta != undefined) ? _.omit(meta, "subject", "replyOn") : {};
                if (ultimateMeta.action == undefined)
                    ultimateMeta.action = i_messaging_client_1.MessagingDefaults.ACTION;
                if (ultimateMeta.correlationId == undefined)
                    ultimateMeta.correlationId = guid_typescript_1.Guid.raw();
                optz.headers = this.convertToNatsHeaders(ultimateMeta);
                // Add auth token header if not already provided. 
                if (((_b = this.apiUserCreds) === null || _b === void 0 ? void 0 : _b.apiToken) && !optz.headers.has(NatsClient_1.DEFAULT_TOKEN_HEADER_PARAM)) {
                    optz.headers.set(NatsClient_1.DEFAULT_TOKEN_HEADER_PARAM, this.apiUserCreds.apiToken);
                }
                // TRIGGER AND WAIT FOR IT...
                let rawRes = yield this.cnxn.request(subject, payload, optz);
                this.logger.debug("Received response to request in topic ".concat(subject, ": ", rawRes.string()));
                result = this.deserialize(rawRes);
            }
            catch (err) {
                result.message = "Error requesting in subject ".concat((_c = meta === null || meta === void 0 ? void 0 : meta.subject) !== null && _c !== void 0 ? _c : i_messaging_client_1.MessagingDefaults.SUBJECT, ": ", (_d = err.name) !== null && _d !== void 0 ? _d : "", (_f = (_e = err.message) !== null && _e !== void 0 ? _e : err.stack) !== null && _f !== void 0 ? _f : "");
                result.error = err;
                this.logger.error(result.message, meta);
            }
            finally {
                return result;
            }
        });
    }
};
NatsClient.defaults = {
    natsHost: "localhost",
    natsPort: 4222,
    serializer: "json",
    requestTimeoutInSecs: 30
};
/** Applied to this.tokenHeaderParam in the ctor.
 *  Typical name of the http request header that declares the authentication token for the request.  */
NatsClient.DEFAULT_TOKEN_HEADER_PARAM = "authorization";
NatsClient = NatsClient_1 = tslib_1.__decorate([
    (0, inversify_1.injectable)(),
    tslib_1.__param(0, (0, inversify_1.inject)(codiac_api_1.ApiUserCreds)),
    tslib_1.__param(1, (0, inversify_1.inject)(NatsClientSettings)),
    tslib_1.__param(2, (0, inversify_1.inject)(codiac_common_1.INTERFACES.IBetterLogger)),
    tslib_1.__metadata("design:paramtypes", [Object, Object, Object])
], NatsClient);
exports.NatsClient = NatsClient;
//# sourceMappingURL=nats-client.js.map