"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.__tsfTypeAliases = exports.LeaderService = void 0;
const tslib_1 = require("tslib");
const _a = tslib_1.__importStar(require("@zyno-io/ts-server-foundation/type-metadata-runtime"));
const node_crypto_1 = require("node:crypto");
const resolver_1 = require("../app/resolver");
const package_1 = require("../helpers/io/package");
const redis_1 = require("../helpers/redis/redis");
const logger_1 = require("./logger");
const __tsf_metadata_type = _a.createCompactMetadataRegistryV1("[1,[{\"kind\":2,\"typeName\":\"() =\\u003e void | Promise\\u003cvoid\\u003e\",\"typeName\":\"LeaderCallback\"},{\"kind\":3},{\"kind\":22,\"type\":{\"kind\":3}},{\"kind\":6},{\"kind\":18,\"typeName\":\"LeaderServiceOptions\",\"types\":[{\"kind\":20,\"name\":\"ttlMs\",\"type\":{\"kind\":12,\"types\":[{\"kind\":7},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"renewalIntervalMs\",\"type\":{\"kind\":12,\"types\":[{\"kind\":7},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"retryDelayMs\",\"type\":{\"kind\":12,\"types\":[{\"kind\":7},{\"kind\":4}]},\"optional\":true}]}]]", []);
const ACQUIRE_SCRIPT = `
if redis.call("exists", KEYS[1]) == 1 then
    return 0
end
redis.call("set", KEYS[1], ARGV[1], "px", ARGV[2])
return 1
`;
const RENEW_SCRIPT = `
if redis.call("get", KEYS[1]) ~= ARGV[1] then
    return 0
end
redis.call("pexpire", KEYS[1], ARGV[2])
return 1
`;
const RELEASE_SCRIPT = `
if redis.call("get", KEYS[1]) ~= ARGV[1] then
    return 0
end
redis.call("del", KEYS[1])
return 1
`;
let redisClient;
function getRedisClient() {
    if (!redisClient) {
        const { client } = (0, redis_1.createRedis)('MUTEX');
        client.defineCommand('ACQUIRE', { lua: ACQUIRE_SCRIPT, numberOfKeys: 1 });
        client.defineCommand('RENEW', { lua: RENEW_SCRIPT, numberOfKeys: 1 });
        client.defineCommand('RELEASE', { lua: RELEASE_SCRIPT, numberOfKeys: 1 });
        redisClient = client;
    }
    return redisClient;
}
let _LeaderServiceMetadata_1;
class LeaderService {
    lockId = (0, node_crypto_1.randomUUID)();
    key;
    _isLeader = false;
    running = false;
    renewTimer = null;
    retryTimer = null;
    becameLeaderCallback = null;
    lostLeaderCallback = null;
    ttlMs;
    renewalIntervalMs;
    retryDelayMs;
    logger = (0, logger_1.createLogger)(this);
    constructor(key, options) {
        const config = (0, resolver_1.getAppConfig)();
        const prefix = config.MUTEX_REDIS_PREFIX ?? config.REDIS_PREFIX ?? (0, package_1.getPackageName)() ?? 'app';
        this.key = `${prefix}:leader:${key}`;
        this.ttlMs = options?.ttlMs ?? 30000;
        this.renewalIntervalMs = options?.renewalIntervalMs ?? 10000;
        this.retryDelayMs = options?.retryDelayMs ?? 5000;
    }
    get isLeader() {
        return this._isLeader;
    }
    setBecameLeaderCallback(callback) {
        this.becameLeaderCallback = callback;
    }
    setLostLeaderCallback(callback) {
        this.lostLeaderCallback = callback;
    }
    start() {
        if (this.running) {
            throw new Error('LeaderService is already running');
        }
        this.running = true;
        this.logger.info('starting leader election', { key: this.key });
        this.tryAcquire();
    }
    async stop() {
        this.running = false;
        if (this.renewTimer) {
            clearInterval(this.renewTimer);
            this.renewTimer = null;
        }
        if (this.retryTimer) {
            clearTimeout(this.retryTimer);
            this.retryTimer = null;
        }
        if (this._isLeader) {
            try {
                await getRedisClient().RELEASE(this.key, this.lockId);
            }
            catch (err) {
                this.logger.warn('failed to release leader lock during stop', { err });
            }
            this._isLeader = false;
        }
    }
    async tryAcquire() {
        if (!this.running)
            return;
        try {
            const result = await getRedisClient().ACQUIRE(this.key, this.lockId, this.ttlMs);
            if (result === 1) {
                // If stop() was called while ACQUIRE was in-flight, release immediately
                if (!this.running) {
                    try {
                        await getRedisClient().RELEASE(this.key, this.lockId);
                    }
                    catch {
                        // ignore - lock will expire via TTL
                    }
                    return;
                }
                this._isLeader = true;
                this.logger.info('became leader', { key: this.key });
                this.startRenewal();
                try {
                    await this.becameLeaderCallback?.();
                }
                catch (err) {
                    this.logger.warn('becameLeader callback error', { err });
                }
                return;
            }
        }
        catch (err) {
            this.logger.warn('error during leader acquisition', { err });
        }
        if (this.running) {
            this.retryTimer = setTimeout(() => this.tryAcquire(), this.retryDelayMs);
        }
    }
    startRenewal() {
        this.renewTimer = setInterval(async () => {
            if (!this.running)
                return;
            try {
                const result = await getRedisClient().RENEW(this.key, this.lockId, this.ttlMs);
                if (result === 0) {
                    this.handleLostLeadership();
                }
            }
            catch (err) {
                this.logger.warn('error during leader renewal', { err });
                this.handleLostLeadership();
            }
        }, this.renewalIntervalMs);
    }
    handleLostLeadership() {
        if (!this._isLeader)
            return;
        this._isLeader = false;
        this.logger.info('lost leadership', { key: this.key });
        if (this.renewTimer) {
            clearInterval(this.renewTimer);
            this.renewTimer = null;
        }
        Promise.resolve()
            .then(() => this.lostLeaderCallback?.())
            .catch(err => {
            this.logger.warn('lostLeader callback error', { err });
        });
        this.lockId = (0, node_crypto_1.randomUUID)();
        if (this.running) {
            this.retryTimer = setTimeout(() => this.tryAcquire(), this.retryDelayMs);
        }
    }
    static __tsfType = _LeaderServiceMetadata_1 = _a.decodeCompactMetadataV1("[1,{\"kind\":16,\"name\":\"LeaderService\",\"typeName\":\"LeaderService\",\"classType\":{\"$tsf\":0},\"properties\":[],\"methods\":[{\"name\":\"setBecameLeaderCallback\",\"parameters\":[{\"name\":\"callback\",\"type\":{\"$tsfType\":0},\"optional\":false,\"default\":false}],\"returnType\":{\"$tsfType\":1}},{\"name\":\"setLostLeaderCallback\",\"parameters\":[{\"name\":\"callback\",\"type\":{\"$tsfType\":0},\"optional\":false,\"default\":false}],\"returnType\":{\"$tsfType\":1}},{\"name\":\"start\",\"parameters\":[],\"returnType\":{\"$tsfType\":1}},{\"name\":\"stop\",\"parameters\":[],\"returnType\":{\"$tsfType\":2}}],\"hasConstructor\":true,\"constructorParameters\":[{\"name\":\"key\",\"type\":{\"$tsfType\":3},\"optional\":false,\"default\":false},{\"name\":\"options\",\"type\":{\"$tsfType\":4},\"optional\":true,\"default\":false}]}]", [() => LeaderService], __tsf_metadata_type);
}
exports.LeaderService = LeaderService;
_LeaderServiceMetadata_1.classType = LeaderService;
LeaderService.__tsfType = _LeaderServiceMetadata_1;
exports.__tsfTypeAliases = _a.decodeCompactMetadataV1("[1,{\"LeaderServiceOptions\":{\"kind\":18,\"typeName\":\"LeaderServiceOptions\",\"types\":[{\"kind\":20,\"name\":\"ttlMs\",\"type\":{\"kind\":12,\"types\":[{\"kind\":7},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"renewalIntervalMs\",\"type\":{\"kind\":12,\"types\":[{\"kind\":7},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"retryDelayMs\",\"type\":{\"kind\":12,\"types\":[{\"kind\":7},{\"kind\":4}]},\"optional\":true}]}}]", [], __tsf_metadata_type);
//# sourceMappingURL=leader.js.map