"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.NatsTransportSession = exports.NatsTransport = exports.NATS_ERROR_SEND_NO_ACK = exports.NATS_ERROR_SEND_FAILED = exports.NATS_ERROR_NOT_CONNECTED = void 0;
const transport_1 = require("./transport");
const nats_1 = require("nats");
const utils_1 = require("@darlean/utils");
exports.NATS_ERROR_NOT_CONNECTED = 'NOT_CONNECTED';
exports.NATS_ERROR_SEND_FAILED = 'SEND_FAILED';
exports.NATS_ERROR_SEND_NO_ACK = 'NO_ACK';
const ACK_TIMEOUT = 4000;
class NatsTransport {
    constructor(deser, seedUrls, ackTimeout) {
        this.deser = deser;
        this.seedUrls = seedUrls;
        this.ackTimeout = ackTimeout ?? ACK_TIMEOUT;
    }
    async connect(appId, onMessage) {
        const session = new NatsTransportSession(this.deser, this.seedUrls, this.ackTimeout);
        await session.connect(appId, onMessage);
        return session;
    }
}
exports.NatsTransport = NatsTransport;
class NatsTransportSession {
    constructor(deser, seedUrls, ackTimeout) {
        this.deser = deser;
        this.seedUrls = seedUrls;
        this.ackTimeout = ackTimeout;
    }
    async connect(appId, onMessage) {
        this.appId = appId;
        this.messageHandler = onMessage;
        this.connection = await (0, nats_1.connect)({ waitOnFirstConnect: true, maxReconnectAttempts: -1, servers: this.seedUrls });
        const subscription = this.connection.subscribe(appId);
        this.subscription = subscription;
        setImmediate(async () => await this.listen(subscription, onMessage));
    }
    async send(envelope, contents, failure) {
        if (envelope.receiverId == this.appId) {
            // Bypass NATS. That is not just a performance optimization. It also ensures that when the error situation is that
            // NATS is not available (like, NATS server not running), the error is properly fed back to the calling code.
            (0, utils_1.deeper)('io.darlean.nats.bypass-nats').perform(() => this.messageHandler?.(envelope, contents, failure));
            return;
        }
        if (!this.connection) {
            this.sendFailureMessage(envelope, 'SEND_ERROR', `Unable to send to  [${envelope.receiverId}]: [${exports.NATS_ERROR_NOT_CONNECTED}]`);
            return;
        }
        const scope = (0, utils_1.currentScope)();
        const msg = {
            envelope,
            contents: contents === undefined ? undefined : this.deser.serialize(contents),
            failure: failure === undefined ? undefined : this.deser.serialize(failure),
            cids: scope.getCorrelationIds(),
            parentUid: scope.getUid()
        };
        try {
            await (0, utils_1.deeper)('io.darlean.nats.send', envelope.receiverId).perform(() => this.connection?.request(envelope.receiverId, this.deser.serialize(msg), {
                timeout: this.ackTimeout || 4000
            }));
        }
        catch (e) {
            const ne = e;
            if (ne.code === nats_1.ErrorCode.NoResponders) {
                this.sendFailureMessage(envelope, transport_1.TRANSPORT_ERROR_UNKNOWN_RECEIVER, `Receiver [${envelope.receiverId}] is not registered to the nats transport`);
            }
            else if (ne.code === nats_1.ErrorCode.Timeout) {
                // It USED TO BE normal to receive a timeout, because we do not send back a reply when we receive a message
                // (that to avoid an unnecessary round trip). We have our own timeout mechanism that will fire when anything
                // else goes wrong. So, we can just ignore the timeout.
                // But we enabled the ack-mechanism ("m.respond()"), so now a timeout is really a signal that something is wrong
                // and that the receiver did not receive the message,
                if (this.ackTimeout > 0) {
                    this.sendFailureMessage(envelope, exports.NATS_ERROR_SEND_NO_ACK, `No ack received after sending to  [${envelope.receiverId}]: [${e}]`);
                }
            }
            else {
                console.log('Error during nats send:', e, envelope.receiverId);
                this.sendFailureMessage(envelope, exports.NATS_ERROR_SEND_FAILED, `Unable to send to  [${envelope.receiverId}]: [${e}]`);
            }
        }
    }
    async finalize() {
        await this.subscription?.drain();
        await this.connection?.close();
    }
    async listen(sub, onMessage) {
        try {
            for await (const m of sub) {
                try {
                    const data = m.data;
                    const msg = this.deser.deserialize(Buffer.from(data));
                    const traceInfo = msg.cids || msg.parentUid
                        ? {
                            correlationIds: msg.cids || [],
                            parentSegmentId: msg.parentUid
                        }
                        : undefined;
                    (0, utils_1.deeper)('io.darlean.nats.received-message', undefined, undefined, traceInfo).perform(() => {
                        // We deliberately DID do not send a response ("m.respond()") because the sender does not need this information.
                        // It would just cost us additional network traffic.
                        // BUT. It appears that during startup, messages may get lost.
                        // Performance tests did not show a significant drop.
                        (0, utils_1.deeper)('io.darlean.nats.send-ack').perform(() => m.respond());
                        const contents = msg.contents ? this.deser.deserialize(msg.contents) : undefined;
                        const failure = msg.failure ? this.deser.deserialize(msg.failure) : undefined;
                        (0, utils_1.deeper)('io.darlean.nats.call-message-handler').perform(() => onMessage(msg.envelope, contents, failure));
                    });
                }
                catch (e) {
                    (0, utils_1.currentScope)().error('Error during handling of incoming message: [Error]', () => ({
                        Error: e
                    }));
                }
            }
        }
        catch (e) {
            console.log('Error during listen', e);
            // Panic!!
            throw e;
        }
    }
    sendFailureMessage(originalEnv, code, msg) {
        const failure = {
            code,
            message: msg
        };
        if (originalEnv.returnEnvelopes) {
            for (const env of originalEnv.returnEnvelopes) {
                (0, utils_1.deeper)('io.darlean.nats.send-failure-message').perform(() => this.send(env, undefined, failure));
            }
        }
    }
}
exports.NatsTransportSession = NatsTransportSession;
