"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.emitDiagnostics = exports.dbgMaintenance = exports.MAINTENANCE_EVENTS = void 0;
const net_1 = require("net");
const promises_1 = require("dns/promises");
const node_assert_1 = __importDefault(require("node:assert"));
const promises_2 = require("node:timers/promises");
const node_diagnostics_channel_1 = __importDefault(require("node:diagnostics_channel"));
exports.MAINTENANCE_EVENTS = {
    PAUSE_WRITING: "pause-writing",
    RESUME_WRITING: "resume-writing",
    TIMEOUTS_UPDATE: "timeouts-update",
};
const PN = {
    MOVING: "MOVING",
    MIGRATING: "MIGRATING",
    MIGRATED: "MIGRATED",
    FAILING_OVER: "FAILING_OVER",
    FAILED_OVER: "FAILED_OVER",
};
const dbgMaintenance = (...args) => {
    if (!process.env.REDIS_DEBUG_MAINTENANCE)
        return;
    return console.log("[MNT]", ...args);
};
exports.dbgMaintenance = dbgMaintenance;
const emitDiagnostics = (event) => {
    if (!process.env.REDIS_EMIT_DIAGNOSTICS)
        return;
    const channel = node_diagnostics_channel_1.default.channel("redis.maintenance");
    channel.publish(event);
};
exports.emitDiagnostics = emitDiagnostics;
class EnterpriseMaintenanceManager {
    #commandsQueue;
    #options;
    #isMaintenance = 0;
    #client;
    static setupDefaultMaintOptions(options) {
        if (options.maintNotifications === undefined) {
            options.maintNotifications =
                options?.RESP === 3 ? "auto" : "disabled";
        }
        if (options.maintEndpointType === undefined) {
            options.maintEndpointType = "auto";
        }
        if (options.maintRelaxedSocketTimeout === undefined) {
            options.maintRelaxedSocketTimeout = 10000;
        }
        if (options.maintRelaxedCommandTimeout === undefined) {
            options.maintRelaxedCommandTimeout = 10000;
        }
    }
    static async getHandshakeCommand(options) {
        if (options.maintNotifications === "disabled")
            return;
        const host = options.url
            ? new URL(options.url).hostname
            : options.socket?.host;
        if (!host)
            return;
        const tls = options.socket?.tls ?? false;
        const movingEndpointType = await determineEndpoint(tls, host, options);
        return {
            cmd: [
                "CLIENT",
                "MAINT_NOTIFICATIONS",
                "ON",
                "moving-endpoint-type",
                movingEndpointType,
            ],
            errorHandler: (error) => {
                (0, exports.dbgMaintenance)("handshake failed:", error);
                if (options.maintNotifications === "enabled") {
                    throw error;
                }
            },
        };
    }
    constructor(commandsQueue, client, options) {
        this.#commandsQueue = commandsQueue;
        this.#options = options;
        this.#client = client;
        this.#commandsQueue.addPushHandler(this.#onPush);
    }
    #onPush = (push) => {
        (0, exports.dbgMaintenance)("ONPUSH:", push.map(String));
        if (!Array.isArray(push) || !["MOVING", "MIGRATING", "MIGRATED", "FAILING_OVER", "FAILED_OVER"].includes(String(push[0]))) {
            return false;
        }
        const type = String(push[0]);
        (0, exports.emitDiagnostics)({
            type,
            timestamp: Date.now(),
            data: {
                push: push.map(String),
            },
        });
        switch (type) {
            case PN.MOVING: {
                // [ 'MOVING', '17', '15', '54.78.247.156:12075' ]
                //             ^seq   ^after    ^new ip
                const afterSeconds = push[2];
                const url = push[3] ? String(push[3]) : null;
                (0, exports.dbgMaintenance)("Received MOVING:", afterSeconds, url);
                this.#onMoving(afterSeconds, url);
                return true;
            }
            case PN.MIGRATING:
            case PN.FAILING_OVER: {
                (0, exports.dbgMaintenance)("Received MIGRATING|FAILING_OVER");
                this.#onMigrating();
                return true;
            }
            case PN.MIGRATED:
            case PN.FAILED_OVER: {
                (0, exports.dbgMaintenance)("Received MIGRATED|FAILED_OVER");
                this.#onMigrated();
                return true;
            }
        }
        return false;
    };
    //  Queue:
    //     toWrite [ C D E ]
    //     waitingForReply [ A B ]   - aka In-flight commands
    //
    //  time: ---1-2---3-4-5-6---------------------------
    //
    //  1. [EVENT] MOVING PN received
    //  2. [ACTION] Pause writing ( we need to wait for new socket to connect and for all in-flight commands to complete )
    //  3. [EVENT] New socket connected
    //  4. [EVENT] In-flight commands completed
    //  5. [ACTION] Destroy old socket
    //  6. [ACTION] Resume writing -> we are going to write to the new socket from now on
    #onMoving = async (afterSeconds, url) => {
        // 1 [EVENT] MOVING PN received
        this.#onMigrating();
        let host;
        let port;
        // The special value `none` indicates that the `MOVING` message doesn’t need
        // to contain an endpoint. Instead it contains the value `null` then. In
        // such a corner case, the client is expected to schedule a graceful
        // reconnect to its currently configured endpoint after half of the grace
        // period that was communicated by the server is over.
        if (url === null) {
            (0, node_assert_1.default)(this.#options.maintEndpointType === "none");
            (0, node_assert_1.default)(this.#options.socket !== undefined);
            (0, node_assert_1.default)("host" in this.#options.socket);
            (0, node_assert_1.default)(typeof this.#options.socket.host === "string");
            host = this.#options.socket.host;
            (0, node_assert_1.default)(typeof this.#options.socket.port === "number");
            port = this.#options.socket.port;
            const waitTime = (afterSeconds * 1000) / 2;
            (0, exports.dbgMaintenance)(`Wait for ${waitTime}ms`);
            await (0, promises_2.setTimeout)(waitTime);
        }
        else {
            const split = url.split(":");
            host = split[0];
            port = Number(split[1]);
        }
        // 2 [ACTION] Pause writing
        (0, exports.dbgMaintenance)("Pausing writing of new commands to old socket");
        this.#client._pause();
        (0, exports.dbgMaintenance)("Creating new tmp client");
        let start = performance.now();
        // If the URL is provided, it takes precedense
        // the options object could just be mutated
        if (this.#options.url) {
            const u = new URL(this.#options.url);
            u.hostname = host;
            u.port = String(port);
            this.#options.url = u.toString();
        }
        else {
            this.#options.socket = {
                ...this.#options.socket,
                host,
                port
            };
        }
        const tmpClient = this.#client.duplicate();
        tmpClient.on('error', (error) => {
            //We dont know how to handle tmp client errors
            (0, exports.dbgMaintenance)(`[ERR]`, error);
        });
        (0, exports.dbgMaintenance)(`Tmp client created in ${(performance.now() - start).toFixed(2)}ms`);
        (0, exports.dbgMaintenance)(`Set timeout for tmp client to ${this.#options.maintRelaxedSocketTimeout}`);
        tmpClient._maintenanceUpdate({
            relaxedCommandTimeout: this.#options.maintRelaxedCommandTimeout,
            relaxedSocketTimeout: this.#options.maintRelaxedSocketTimeout,
        });
        (0, exports.dbgMaintenance)(`Connecting tmp client: ${host}:${port}`);
        start = performance.now();
        await tmpClient.connect();
        (0, exports.dbgMaintenance)(`Connected to tmp client in ${(performance.now() - start).toFixed(2)}ms`);
        // 3 [EVENT] New socket connected
        (0, exports.dbgMaintenance)(`Wait for all in-flight commands to complete`);
        await this.#commandsQueue.waitForInflightCommandsToComplete();
        (0, exports.dbgMaintenance)(`In-flight commands completed`);
        // 4 [EVENT] In-flight commands completed
        (0, exports.dbgMaintenance)("Swap client sockets...");
        const oldSocket = this.#client._ejectSocket();
        const newSocket = tmpClient._ejectSocket();
        this.#client._insertSocket(newSocket);
        tmpClient._insertSocket(oldSocket);
        tmpClient.destroy();
        (0, exports.dbgMaintenance)("Swap client sockets done.");
        // 5 + 6
        (0, exports.dbgMaintenance)("Resume writing");
        this.#client._unpause();
        this.#onMigrated();
    };
    #onMigrating = () => {
        this.#isMaintenance++;
        if (this.#isMaintenance > 1) {
            (0, exports.dbgMaintenance)(`Timeout relaxation already done`);
            return;
        }
        const update = {
            relaxedCommandTimeout: this.#options.maintRelaxedCommandTimeout,
            relaxedSocketTimeout: this.#options.maintRelaxedSocketTimeout,
        };
        this.#client._maintenanceUpdate(update);
    };
    #onMigrated = () => {
        //ensure that #isMaintenance doesnt go under 0
        this.#isMaintenance = Math.max(this.#isMaintenance - 1, 0);
        if (this.#isMaintenance > 0) {
            (0, exports.dbgMaintenance)(`Not ready to unrelax timeouts yet`);
            return;
        }
        const update = {
            relaxedCommandTimeout: undefined,
            relaxedSocketTimeout: undefined
        };
        this.#client._maintenanceUpdate(update);
    };
}
exports.default = EnterpriseMaintenanceManager;
function isPrivateIP(ip) {
    const version = (0, net_1.isIP)(ip);
    if (version === 4) {
        const octets = ip.split(".").map(Number);
        return (octets[0] === 10 ||
            (octets[0] === 172 && octets[1] >= 16 && octets[1] <= 31) ||
            (octets[0] === 192 && octets[1] === 168));
    }
    if (version === 6) {
        return (ip.startsWith("fc") || // Unique local
            ip.startsWith("fd") || // Unique local
            ip === "::1" || // Loopback
            ip.startsWith("fe80") // Link-local unicast
        );
    }
    return false;
}
async function determineEndpoint(tlsEnabled, host, options) {
    (0, node_assert_1.default)(options.maintEndpointType !== undefined);
    if (options.maintEndpointType !== "auto") {
        (0, exports.dbgMaintenance)(`Determine endpoint type: ${options.maintEndpointType}`);
        return options.maintEndpointType;
    }
    const ip = (0, net_1.isIP)(host) ? host : (await (0, promises_1.lookup)(host, { family: 0 })).address;
    const isPrivate = isPrivateIP(ip);
    let result;
    if (tlsEnabled) {
        result = isPrivate ? "internal-fqdn" : "external-fqdn";
    }
    else {
        result = isPrivate ? "internal-ip" : "external-ip";
    }
    (0, exports.dbgMaintenance)(`Determine endpoint type: ${result}`);
    return result;
}
//# sourceMappingURL=enterprise-maintenance-manager.js.map