"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.MeshService = exports.MeshNoHandlerError = exports.MeshHandlerError = exports.MeshRequestTimeoutError = exports.__ΩMeshServiceOptions = exports.__ΩMeshNode = exports.__ΩMeshBroadcastOptions = exports.__ΩMeshBroadcastMap = exports.__ΩMeshMessageMap = void 0;
exports.destroyMeshRedis = destroyMeshRedis;
const __ΩReturnType = ['T', 'args', '', 'ReturnType', 'l>e"!R"RPde#!P"@2"h"!/#qk#\'QRb!Pde"!p)w$y'];
const __ΩRecord = ['K', 'T', 'Record', 'l\'e#"Rb!b"Pde"!N#!w#y'];
const __ΩError = ['name', 'message', 'stack', 'Error', 'P&4!&4"&4#8Mw$y'];
function __assignType(fn, args) {
    fn.__type = args;
    return fn;
}
const type_1 = require("@deepkit/type");
const os_1 = require("os");
const redis_1 = require("../helpers/redis/redis");
/*@ts-ignore*/
var { __ΩLeaderServiceOptions } = require('./leader');
const leader_1 = require("./leader");
const logger_1 = require("./logger");
const __ΩMeshMessageMap = [() => __ΩRecord, 'request', 'response', 'MeshMessageMap', '&P#4"#4#Mo!#w$y'];
exports.__ΩMeshMessageMap = __ΩMeshMessageMap;
const __ΩMeshBroadcastMap = [() => __ΩRecord, 'MeshBroadcastMap', '&#o!#w"y'];
exports.__ΩMeshBroadcastMap = __ΩMeshBroadcastMap;
const __ΩMeshBroadcastOptions = ['skipSelf', 'MeshBroadcastOptions', 'P)4!8Mw"y'];
exports.__ΩMeshBroadcastOptions = __ΩMeshBroadcastOptions;
const __ΩMeshNode = ['instanceId', 'hostname', 'self', 'MeshNode', 'P\'4!&4")4#Mw$y'];
exports.__ΩMeshNode = __ΩMeshNode;
const __ΩMeshServiceOptions = ['heartbeatIntervalMs', 'nodeTtlMs', 'requestTimeoutMs', () => __ΩLeaderServiceOptions, 'leaderOptions', 'MeshServiceOptions', 'P\'4!8\'4"8\'4#8n$4%8Mw&y'];
exports.__ΩMeshServiceOptions = __ΩMeshServiceOptions;
class MeshRequestTimeoutError extends Error {
    constructor(instanceId, type) {
        super(`Mesh request to instance ${instanceId} timed out (type: ${type})`);
        this.name = 'MeshRequestTimeoutError';
    }
}
exports.MeshRequestTimeoutError = MeshRequestTimeoutError;
MeshRequestTimeoutError.__type = [() => Error, 'instanceId', 'type', 'constructor', 'MeshRequestTimeoutError', 'P7!P\'2"&2#"0$5w%'];
class MeshHandlerError extends Error {
    constructor(message) {
        super(message);
        this.name = 'MeshHandlerError';
    }
}
exports.MeshHandlerError = MeshHandlerError;
MeshHandlerError.__type = [() => Error, 'message', 'constructor', 'MeshHandlerError', 'P7!P&2""0#5w$'];
class MeshNoHandlerError extends Error {
    constructor(type) {
        super(`No handler registered for mesh message type: ${type}`);
        this.name = 'MeshNoHandlerError';
    }
}
exports.MeshNoHandlerError = MeshNoHandlerError;
MeshNoHandlerError.__type = [() => Error, 'type', 'constructor', 'MeshNoHandlerError', 'P7!P&2""0#5w$'];
// --- Constants ---
const NO_HANDLER_ERROR_PREFIX = 'MESH_NO_HANDLER:';
// --- Lua Scripts ---
const HEARTBEAT_SCRIPT = `
local time = redis.call("time")
local now_ms = tonumber(time[1]) * 1000 + math.floor(tonumber(time[2]) / 1000)
redis.call("zadd", KEYS[1], now_ms, ARGV[1])
return 1
`;
const CLEANUP_SCRIPT = `
local time = redis.call("time")
local now_ms = tonumber(time[1]) * 1000 + math.floor(tonumber(time[2]) / 1000)
local cutoff = now_ms - tonumber(ARGV[1])
local expired = redis.call("zrangebyscore", KEYS[1], "-inf", cutoff)
for i, id in ipairs(expired) do
    redis.call("zrem", KEYS[1], id)
    redis.call("hdel", KEYS[2], id)
end
return expired
`;
const DEREGISTER_SCRIPT = `
redis.call("zrem", KEYS[1], ARGV[1])
redis.call("hdel", KEYS[2], ARGV[1])
return 1
`;
const __ΩMeshRedisClient = [() => __ΩReturnType, () => redis_1.createRedis, "client", 'key', 'instanceId', '', 'HEARTBEAT', 'heartbeatsKey', 'nodesKey', 'ttlMs', 'CLEANUP', 'DEREGISTER', 'MeshRedisClient', 'Pi"o!".#fPP&2$&2%\'`/&4\'P&2(&2)&2*&F`/&4+P&2(&2)&2%\'`/&4,MKw-y'];
let meshRedis = null;
function getMeshRedis() {
    if (!meshRedis) {
        const { client, prefix } = (0, redis_1.createRedis)('MESH');
        client.defineCommand('HEARTBEAT', { lua: HEARTBEAT_SCRIPT, numberOfKeys: 1 });
        client.defineCommand('CLEANUP', { lua: CLEANUP_SCRIPT, numberOfKeys: 2 });
        client.defineCommand('DEREGISTER', { lua: DEREGISTER_SCRIPT, numberOfKeys: 2 });
        meshRedis = { client: client, prefix };
    }
    return meshRedis;
}
getMeshRedis.__type = [() => __ΩMeshRedisClient, 'client', 'prefix', 'getMeshRedis', 'PPn!4"&4#M/$'];
function destroyMeshRedis() {
    if (meshRedis) {
        meshRedis.client.disconnect();
        meshRedis = null;
    }
}
destroyMeshRedis.__type = ['destroyMeshRedis', 'P$/!'];
const __ΩMeshRequest = ['requestId', 'senderInstanceId', 'type', 'data', 'timeoutMs', 'MeshRequest', 'P&4!\'4"&4##4$\'4%8Mw&y'];
const __ΩMeshResponse = ['requestId', true, 'reply', 'data', 'error', 'MeshResponse', 'P&4!."4##4$8&4%8Mw&y'];
const __ΩMeshHeartbeat = ['requestId', true, 'heartbeat', 'MeshHeartbeat', 'P&4!."4#Mw$y'];
const __ΩMeshBroadcast = [true, 'broadcast', 'senderInstanceId', 'type', 'data', 'MeshBroadcast', 'P.!4"\'4#&4$#4%Mw&y'];
const __ΩPendingRequest = ['value', '', 'resolve', () => __ΩError, 'reason', 'reject', () => __ΩReturnType, () => setTimeout, 'timer', 'type', 'targetInstanceId', 'timeoutMs', 'PendingRequest', 'PP#2!$/"4#Pn$2%$/"4&i(o\'"4)&4*\'4+\'4,Mw-y'];
// --- MeshService ---
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
class MeshService {
    constructor(key, options) {
        this._instanceId = 0;
        this.prefix = '';
        this.running = false;
        this.heartbeatTimer = null;
        this.leaderService = null;
        this.subscriberClient = null;
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        this.handlers = (Map.Ω = [['&'], ['data', '', 'P"2!"/"']], new Map());
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        this.broadcastHandlers = (Map.Ω = [['&'], ['data', 'senderInstanceId', '', 'P"2!\'2"P$$`J/#']], new Map());
        this.pendingRequests = (Map.Ω = [['&'], [() => __ΩPendingRequest, 'n!']], new Map());
        this.activeHandlerIntervals = (Set.Ω = [[() => __ΩReturnType, () => setInterval, 'i"o!"']], new Set());
        this.nodeCleanedUpCallback = null;
        this.logger = (0, logger_1.createLogger)(this);
        this.key = key;
        this.heartbeatIntervalMs = options?.heartbeatIntervalMs ?? 5000;
        this.nodeTtlMs = options?.nodeTtlMs ?? 15000;
        this.requestTimeoutMs = options?.requestTimeoutMs ?? 10000;
        this.leaderOptions = options?.leaderOptions;
    }
    get instanceId() {
        return this._instanceId;
    }
    registerHandler(type, handler) {
        this.handlers.set(type, handler);
    }
    registerBroadcastHandler(type, handler) {
        this.broadcastHandlers.set(type, handler);
    }
    setNodeCleanedUpCallback(cb) {
        this.nodeCleanedUpCallback = cb;
    }
    async getNodes() {
        if (!this.running) {
            throw new Error('MeshService is not running');
        }
        const { client } = getMeshRedis();
        const members = await client.zrange(this.heartbeatsKey(), 0, -1);
        if (members.length === 0)
            return [];
        const hostnames = await client.hmget(this.nodesKey(), ...members);
        return members.map(__assignType((id, i) => ({
            instanceId: parseInt(id, 10),
            hostname: hostnames[i] ?? 'unknown',
            self: parseInt(id, 10) === this._instanceId
        }), ['id', 'i', '', 'P"2!"2""/#']));
    }
    async invoke(instanceId, type, data, timeoutMs) {
        if (!this.running) {
            throw new Error('MeshService is not running');
        }
        const effectiveTimeout = timeoutMs ?? this.requestTimeoutMs;
        // Local invocation — call handler directly
        if (instanceId === this._instanceId) {
            const handler = this.handlers.get(type);
            if (!handler)
                throw new MeshNoHandlerError(type);
            return handler(data);
        }
        // Remote invocation via pub/sub
        const requestId = (0, type_1.uuid)();
        const channel = this.channelForInstance(instanceId);
        // Serialize before registering pending request to avoid leaked timers on stringify failure
        const message = {
            requestId,
            senderInstanceId: this._instanceId,
            type,
            data,
            timeoutMs: effectiveTimeout
        };
        const payload = JSON.stringify(message);
        return (Promise.Ω = [[() => type, () => data, "request", "response", '!Pdi!h!!qe!!!jPdi"!h!!f.#fqe!!!jf.$f']], new Promise(__assignType((resolve, reject) => {
            const timer = setTimeout(() => {
                this.pendingRequests.delete(requestId);
                reject(new MeshRequestTimeoutError(instanceId, type));
            }, effectiveTimeout);
            this.pendingRequests.set(requestId, {
                resolve: resolve,
                reject,
                timer,
                type,
                targetInstanceId: instanceId,
                timeoutMs: effectiveTimeout
            });
            const { client } = getMeshRedis();
            client.publish(channel, payload).catch(__assignType(err => {
                clearTimeout(timer);
                this.pendingRequests.delete(requestId);
                reject(new Error(`Failed to publish mesh request: ${err instanceof Error ? err.message : String(err)}`));
            }, ['err', '', 'P"2!"/"']));
        }, ['resolve', 'reject', '', 'P"2!"2""/#'])));
    }
    async broadcast(type, data, options) {
        if (!this.running) {
            throw new Error('MeshService is not running');
        }
        const message = {
            broadcast: true,
            senderInstanceId: this._instanceId,
            type,
            data
        };
        // Deliver locally unless skipSelf
        if (!options?.skipSelf) {
            this.handleBroadcastMessage(message);
        }
        const { client } = getMeshRedis();
        await client.publish(this.broadcastChannel(), JSON.stringify(message));
    }
    async start() {
        if (this.running) {
            throw new Error('MeshService is already running');
        }
        const { client, prefix } = getMeshRedis();
        this.prefix = prefix;
        // Acquire unique instance ID
        this._instanceId = await client.incr(this.nextIdKey());
        // Create subscriber and subscribe to own channel
        const { client: subClient } = (0, redis_1.createRedis)('MESH');
        this.subscriberClient = subClient;
        try {
            const instanceChannel = this.channelForInstance(this._instanceId);
            const broadcastCh = this.broadcastChannel();
            await subClient.subscribe(instanceChannel, broadcastCh);
            subClient.on('message', __assignType((channel, message) => {
                if (channel === broadcastCh) {
                    this.handleBroadcastIncoming(message);
                }
                else {
                    this.handleMessage(message);
                }
            }, ['channel', 'message', '', 'P&2!&2""/#']));
            // Register heartbeat and node metadata
            await client.HEARTBEAT(this.heartbeatsKey(), String(this._instanceId));
            await client.hset(this.nodesKey(), String(this._instanceId), (0, os_1.hostname)());
        }
        catch (err) {
            // Clean up subscriber on partial init failure
            try {
                await subClient.unsubscribe();
                await subClient.quit();
            }
            catch {
                // ignore cleanup errors
            }
            this.subscriberClient = null;
            throw err;
        }
        this.running = true;
        // Start heartbeat interval
        this.heartbeatTimer = setInterval(() => this.doHeartbeat(), this.heartbeatIntervalMs);
        // Start leader service for cleanup duties
        this.leaderService = new leader_1.LeaderService(`mesh:${this.key}`, this.leaderOptions);
        this.leaderService.start();
        this.logger.info('mesh node started', { instanceId: this._instanceId, key: this.key });
    }
    async stop() {
        this.running = false;
        // Stop leader service
        if (this.leaderService) {
            await this.leaderService.stop();
            this.leaderService = null;
        }
        // Stop heartbeat
        if (this.heartbeatTimer) {
            clearInterval(this.heartbeatTimer);
            this.heartbeatTimer = null;
        }
        // Clear all active handler heartbeat intervals
        for (const interval of this.activeHandlerIntervals) {
            clearInterval(interval);
        }
        this.activeHandlerIntervals.clear();
        // Reject all pending requests
        for (const [requestId, pending] of this.pendingRequests) {
            clearTimeout(pending.timer);
            pending.reject(new Error('MeshService stopped'));
            this.pendingRequests.delete(requestId);
        }
        // Unsubscribe and quit subscriber
        if (this.subscriberClient) {
            try {
                await this.subscriberClient.unsubscribe();
                await this.subscriberClient.quit();
            }
            catch {
                // ignore errors during cleanup
            }
            this.subscriberClient = null;
        }
        // Atomically remove self from heartbeats and nodes
        if (this._instanceId !== 0) {
            const stoppedInstanceId = this._instanceId;
            this._instanceId = 0;
            try {
                const { client } = getMeshRedis();
                await client.DEREGISTER(this.heartbeatsKey(), this.nodesKey(), String(stoppedInstanceId));
            }
            catch {
                // ignore errors during cleanup
            }
            this.logger.info('mesh node stopped', { instanceId: stoppedInstanceId, key: this.key });
        }
    }
    // --- Private ---
    nextIdKey() {
        return `${this.prefix}:mesh:${this.key}:next_id`;
    }
    heartbeatsKey() {
        return `${this.prefix}:mesh:${this.key}:heartbeats`;
    }
    nodesKey() {
        return `${this.prefix}:mesh:${this.key}:nodes`;
    }
    channelForInstance(instanceId) {
        return `${this.prefix}:mesh:${this.key}:node:${instanceId}`;
    }
    broadcastChannel() {
        return `${this.prefix}:mesh:${this.key}:broadcast`;
    }
    async doHeartbeat() {
        if (!this.running)
            return;
        try {
            const { client } = getMeshRedis();
            await client.HEARTBEAT(this.heartbeatsKey(), String(this._instanceId));
        }
        catch (err) {
            this.logger.warn('mesh heartbeat failed', { err });
        }
        // If leader, run cleanup
        if (this.leaderService?.isLeader) {
            await this.doCleanup();
        }
    }
    async doCleanup() {
        try {
            const { client } = getMeshRedis();
            const expired = await client.CLEANUP(this.heartbeatsKey(), this.nodesKey(), String(this.nodeTtlMs));
            if (expired.length > 0) {
                this.logger.info('cleaned up expired mesh nodes', { expired });
                for (const idStr of expired) {
                    const id = parseInt(idStr, 10);
                    try {
                        await this.nodeCleanedUpCallback?.(id);
                    }
                    catch (err) {
                        this.logger.warn('node cleanup callback error', { err, instanceId: id });
                    }
                }
            }
        }
        catch (err) {
            this.logger.warn('mesh cleanup failed', { err });
        }
    }
    handleBroadcastIncoming(raw) {
        if (!this.running)
            return;
        let msg;
        try {
            msg = JSON.parse(raw);
        }
        catch {
            return;
        }
        if (typeof msg !== 'object' || msg === null)
            return;
        const obj = msg;
        // Skip self-sent broadcasts (we already delivered locally in broadcast())
        if (obj.senderInstanceId === this._instanceId)
            return;
        this.handleBroadcastMessage(obj);
    }
    handleBroadcastMessage(msg) {
        const handler = this.broadcastHandlers.get(msg.type);
        if (!handler)
            return;
        Promise.resolve()
            .then(() => handler(msg.data, msg.senderInstanceId))
            .catch(__assignType(err => {
            this.logger.warn('broadcast handler error', { err, type: msg.type });
        }, ['err', '', 'P"2!"/"']));
    }
    handleMessage(raw) {
        if (!this.running)
            return;
        let msg;
        try {
            msg = JSON.parse(raw);
        }
        catch (err) {
            this.logger.warn('failed to parse mesh message', { err, raw });
            return;
        }
        if (typeof msg !== 'object' || msg === null) {
            this.logger.warn('invalid mesh message: not an object', { raw });
            return;
        }
        const obj = msg;
        if ('heartbeat' in obj && obj.heartbeat) {
            this.handleHeartbeatMessage(obj);
        }
        else if ('reply' in obj && obj.reply) {
            this.handleResponseMessage(obj);
        }
        else {
            this.handleRequestMessage(obj);
        }
    }
    handleHeartbeatMessage(msg) {
        const pending = this.pendingRequests.get(msg.requestId);
        if (!pending)
            return;
        // Reset timeout using the original per-request timeout
        clearTimeout(pending.timer);
        pending.timer = setTimeout(() => {
            this.pendingRequests.delete(msg.requestId);
            pending.reject(new MeshRequestTimeoutError(pending.targetInstanceId, pending.type));
        }, pending.timeoutMs);
    }
    handleResponseMessage(msg) {
        const pending = this.pendingRequests.get(msg.requestId);
        if (!pending)
            return;
        clearTimeout(pending.timer);
        this.pendingRequests.delete(msg.requestId);
        if (msg.error !== undefined) {
            if (typeof msg.error === 'string' && msg.error.startsWith(NO_HANDLER_ERROR_PREFIX)) {
                pending.reject(new MeshNoHandlerError(msg.error.slice(NO_HANDLER_ERROR_PREFIX.length)));
            }
            else {
                pending.reject(new MeshHandlerError(msg.error));
            }
        }
        else {
            pending.resolve(msg.data);
        }
    }
    handleRequestMessage(msg) {
        if (!this.running)
            return;
        if (typeof msg.requestId !== 'string' || typeof msg.senderInstanceId !== 'number' || typeof msg.type !== 'string') {
            this.logger.warn('invalid mesh request: missing or malformed fields', { msg });
            return;
        }
        const handler = this.handlers.get(msg.type);
        const { client } = getMeshRedis();
        const senderChannel = this.channelForInstance(msg.senderInstanceId);
        if (!handler) {
            const response = {
                requestId: msg.requestId,
                reply: true,
                error: `${NO_HANDLER_ERROR_PREFIX}${msg.type}`
            };
            client.publish(senderChannel, JSON.stringify(response)).catch(__assignType(err => {
                this.logger.warn('failed to publish no-handler response', { err });
            }, ['err', '', 'P"2!"/"']));
            return;
        }
        // Use the caller's timeout for heartbeat interval (fall back to our own)
        const effectiveTimeoutMs = msg.timeoutMs ?? this.requestTimeoutMs;
        const heartbeatInterval = setInterval(() => {
            const heartbeat = {
                requestId: msg.requestId,
                heartbeat: true
            };
            client.publish(senderChannel, JSON.stringify(heartbeat)).catch(__assignType(err => {
                this.logger.warn('failed to publish handler heartbeat', { err });
            }, ['err', '', 'P"2!"/"']));
        }, effectiveTimeoutMs * 0.75);
        this.activeHandlerIntervals.add(heartbeatInterval);
        // Execute handler
        Promise.resolve()
            .then(() => handler(msg.data))
            .then(__assignType(result => {
            clearInterval(heartbeatInterval);
            this.activeHandlerIntervals.delete(heartbeatInterval);
            const response = {
                requestId: msg.requestId,
                reply: true,
                data: result
            };
            client.publish(senderChannel, JSON.stringify(response)).catch(__assignType(err => {
                this.logger.warn('failed to publish handler response', { err });
            }, ['err', '', 'P"2!"/"']));
        }, ['result', '', 'P"2!"/"']))
            .catch(__assignType(err => {
            clearInterval(heartbeatInterval);
            this.activeHandlerIntervals.delete(heartbeatInterval);
            const response = {
                requestId: msg.requestId,
                reply: true,
                error: err instanceof Error ? err.message : String(err)
            };
            client.publish(senderChannel, JSON.stringify(response)).catch(__assignType(publishErr => {
                this.logger.warn('failed to publish handler error response', { err: publishErr });
            }, ['publishErr', '', 'P"2!"/"']));
        }, ['err', '', 'P"2!"/"']));
    }
}
exports.MeshService = MeshService;
MeshService.__type = ['T', 'B', '_instanceId', function () { return 0; }, 'key', 'prefix', function () { return ''; }, 'running', function () { return false; }, 'heartbeatIntervalMs', 'nodeTtlMs', 'requestTimeoutMs', () => __ΩLeaderServiceOptions, 'leaderOptions', () => __ΩReturnType, () => setInterval, 'heartbeatTimer', function () { return null; }, () => leader_1.LeaderService, 'leaderService', function () { return null; }, () => __ΩReturnType, () => redis_1.createRedis, "client", 'subscriberClient', function () { return null; }, 'handlers', function () { return (Map.Ω = [['&'], ['data', '', 'P"2!"/"']], new Map()); }, 'broadcastHandlers', function () { return (Map.Ω = [['&'], ['data', 'senderInstanceId', '', 'P"2!\'2"P$$`J/#']], new Map()); }, 'pendingRequests', function () { return (Map.Ω = [['&'], [() => __ΩPendingRequest, 'n!']], new Map()); }, 'activeHandlerIntervals', function () { return (Set.Ω = [[() => __ΩReturnType, () => setInterval, 'i"o!"']], new Set()); }, 'instanceId', '', 'nodeCleanedUpCallback', function () { return null; }, 'logger', function () { return (0, logger_1.createLogger)(this); }, () => __ΩMeshServiceOptions, 'options', 'constructor', 'type', "request", 'data', "response", "response", 'handler', 'registerHandler', 'senderInstanceId', 'registerBroadcastHandler', 'cb', 'setNodeCleanedUpCallback', () => __ΩMeshNode, 'getNodes', "request", 'timeoutMs', "response", 'invoke', () => __ΩMeshBroadcastOptions, 'broadcast', 'start', 'stop', 'nextIdKey', 'heartbeatsKey', 'nodesKey', 'channelForInstance', 'broadcastChannel', 'doHeartbeat', 'doCleanup', 'raw', 'handleBroadcastIncoming', () => __ΩMeshBroadcast, 'msg', 'handleBroadcastMessage', 'handleMessage', () => __ΩMeshHeartbeat, 'handleHeartbeatMessage', () => __ΩMeshResponse, 'handleResponseMessage', () => __ΩMeshRequest, 'handleRequestMessage', 'MeshService', 'b!PMc"\'3#;>$&3%;&3&;>\')3(;>)\'3*;\'3+;\'3,;n-3.8;Pi0o/",J31;>2PP73,J34;>5Pi7o6".8f,J39;>:!3;;><!3=;>>!3?;>@!3A;>BPP\'2CP$$`J/D,J3E;>F!3G;>HP&2%nI2J8"0K!P"2LPe#!"f.Mf2NPe$!"f.Ofe$!"f.Pf`J/D2Q$0RP"2LPe#""f2N\'2SP$$`J/D2Q$0TPP\'2CP$$`J/D2U$0VPnWF`0XP\'2C"2Le"!"f.Yf2N\'2Z8e"!"f.[f`0\\P"2Le"""f2Nn]2J8$`0^P$`0_P$`0`P&0a;P&0b;P&0c;P\'2C&0d;P&0e;P$`0f;P$`0g;P&2h$0i;Pnj2k$0l;P&2h$0m;Pnn2k$0o;Pnp2k$0q;Pnr2k$0s;5wt'];
//# sourceMappingURL=mesh.js.map