"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.MutexAcquisitionError = exports.__ΩMutexKey = void 0;
exports.withMutex = withMutex;
exports.withMutexes = withMutexes;
exports.flattenMutexKey = flattenMutexKey;
const __ΩOmit = ['T', 'K', () => __ΩPick, () => __ΩExclude, 'Omit', 'b!b"e!!e!!ge!"o$#o##w%y'];
const __ΩInstanceType = ['T', 'args', 'new', 'InstanceType', 'l>e"!R"RPde#!P"@2"h"!/#qk#\'QRb!Pde"!p)w$y'];
const __ΩPick = ['T', 'K', 'Pick', 'l+e#!e"!fRb!b"Pde""N#!w#y'];
const __ΩExclude = ['T', 'U', 'Exclude', 'l6!Re$!RPe#!e$"qk#%QRb!b"Pde"!p)w#y'];
function __assignType(fn, args) {
    fn.__type = args;
    return fn;
}
const type_1 = require("@deepkit/type");
const crypto_1 = require("crypto");
const lodash_1 = require("lodash");
const resolver_1 = require("../../app/resolver");
const telemetry_1 = require("../../telemetry");
/*@ts-ignore*/
var { __ΩConcretePrimitive } = require('../../types');
const package_1 = require("../io/package");
const date_1 = require("../utils/date");
const redis_1 = require("./redis");
const __ΩMutexFn = ['R', 'didWait', '', 'MutexFn', '"c!P)2"e"!`/#w$y'];
const __ΩMutexKey = [() => __ΩConcretePrimitive, () => __ΩInstanceType, 'MutexKey', 'Pn!"o""Jw#y'];
exports.__ΩMutexKey = __ΩMutexKey;
const getMutexHelpers = (0, lodash_1.memoize)(() => {
    const appConfig = (0, resolver_1.getAppConfig)();
    if (appConfig.MUTEX_MODE === 'local') {
        return {
            prepareKey: __assignType((k) => k, ['k', '', 'P&2!"/"']),
            exec: localMutexExec
        };
    }
    const prefix = appConfig.MUTEX_REDIS_PREFIX ?? appConfig.REDIS_PREFIX ?? (0, package_1.getPackageName)() ?? 'app';
    return {
        prepareKey: __assignType((k) => `${prefix}:${k}`, ['k', '', 'P&2!"/"']),
        exec: redisMutexExec
    };
});
const __ΩIMutexOptions = ['T', () => __ΩMutexKey, () => __ΩMutexKey, 'key', () => __ΩMutexFn, 'fn', 'retryCount', 'retryDelay', 'renewInterval', 'IMutexOptions', 'b!PPn"n#FJ4$e"!o%"4&\'4\'8\'4(8\'4)8Mw*y'];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const DefaultMutexOptions = {
    retryCount: 30,
    retryDelay: 1000,
    renewInterval: 1000
};
class MutexAcquisitionError extends Error {
    constructor(message) {
        super(message ?? 'Failed to acquire mutex within timeout');
        this.name = 'MutexAcquisitionError';
    }
}
exports.MutexAcquisitionError = MutexAcquisitionError;
MutexAcquisitionError.__type = [() => Error, 'message', 'constructor', 'MutexAcquisitionError', 'P7!P&2"8"0#5w$'];
function withMutex(options) {
    const flattenedKey = flattenMutexKey(options.key);
    const helpers = getMutexHelpers();
    const preparedKey = helpers.prepareKey(flattenedKey);
    return (0, telemetry_1.withSpan)('mutex:requested', { key: flattenedKey }, () => helpers.exec({
        ...DefaultMutexOptions,
        ...options,
        key: preparedKey,
        fn: __assignType((didWait) => (0, telemetry_1.withSpan)('mutex:held', () => options.fn(didWait)), ['didWait', '', 'P)2!"/"'])
    }));
}
withMutex.__type = [() => __ΩIMutexOptions, 'options', 'withMutex', 'P"o!"2""`/#'];
function withMutexes(options) {
    const currentKey = options.keys[0];
    const remainingKeys = options.keys.slice(1);
    let didAnyWait = false;
    return withMutex({
        ...options,
        key: currentKey,
        fn: __assignType(didWait => {
            didAnyWait = didAnyWait || didWait;
            return remainingKeys.length ? withMutexes({ ...options, keys: remainingKeys }) : options.fn(didAnyWait);
        }, ['didWait', '', 'P"2!"/"'])
    });
}
withMutexes.__type = [() => __ΩOmit, () => __ΩIMutexOptions, "key", () => __ΩIMutexOptions, "key", 'keys', 'options', 'withMutexes', 'PP"o"".#o!#P"o$".%fF4&MK2\'"`/('];
/**
 * Local Mutex
 */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const localMutexes = (Map.Ω = [['&'], ['"`']], new Map());
async function localMutexExec(options) {
    const hasMutex = localMutexes.has(options.key);
    if (localMutexes.has(options.key)) {
        // eslint-disable-next-line no-async-promise-executor
        await (Promise.Ω = [['$']], new Promise(__assignType(async (resolve, reject) => {
            let hasTimedOut = false;
            const timeout = setTimeout(() => {
                hasTimedOut = true;
                reject(new MutexAcquisitionError());
            }, options.retryDelay * options.retryCount);
            while (localMutexes.has(options.key)) {
                try {
                    await localMutexes.get(options.key);
                }
                catch {
                    /**/
                }
            }
            if (hasTimedOut)
                return;
            clearTimeout(timeout);
            resolve();
        }, ['resolve', 'reject', '', 'P"2!"2""/#'])));
    }
    const promise = (Promise.Ω = [[() => options, () => __ΩIMutexOptions, 'Pdi!h!!o""qe!!!j']], new Promise(__assignType((resolve, reject) => {
        options
            .fn(hasMutex)
            .then(resolve)
            .catch(reject)
            .finally(() => {
            localMutexes.delete(options.key);
        });
    }, ['resolve', 'reject', '', 'P"2!"2""/#'])));
    localMutexes.set(options.key, promise);
    return promise;
}
localMutexExec.__type = [() => __ΩIMutexOptions, 'options', 'localMutexExec', 'P"o!"2""`/#'];
/**
 * Redis Mutex
 */
// Redis scripts:
// ACQUIRE [key] [value] [ttl]
// RENEW [key] [value] [ttl]
// RELEASE [key] [value]
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
`;
const getMutexRedisClient = (0, lodash_1.memoize)(() => {
    const { client } = (0, redis_1.createRedis)('MUTEX');
    return client;
});
const getRedisClient = (0, lodash_1.memoize)(() => {
    const redisClient = getMutexRedisClient();
    redisClient.defineCommand('ACQUIRE', {
        lua: ACQUIRE_SCRIPT,
        numberOfKeys: 1
    });
    redisClient.defineCommand('RENEW', {
        lua: RENEW_SCRIPT,
        numberOfKeys: 1
    });
    redisClient.defineCommand('RELEASE', {
        lua: RELEASE_SCRIPT,
        numberOfKeys: 1
    });
    return redisClient;
});
async function redisMutexExec(options) {
    const lockId = (0, type_1.uuid)();
    const redisClient = getRedisClient();
    const lockTtl = options.renewInterval * 3;
    const deadlineMs = Date.now() + options.retryCount * options.retryDelay;
    let attempts = 0;
    do {
        attempts++;
        const result = await redisClient.ACQUIRE(options.key, lockId, lockTtl);
        if (result === 0) {
            await (0, date_1.sleepMs)(options.retryDelay);
            continue;
        }
        return (Promise.Ω = [[() => options, () => __ΩIMutexOptions, 'Pdi!h!!o""qe!!!j']], new Promise(__assignType((resolve, reject) => {
            const interval = setInterval(() => {
                redisClient
                    .RENEW(options.key, lockId, lockTtl)
                    .then(__assignType(result => {
                    if (result === 0)
                        throw new MutexAcquisitionError('Key missing or value mismatch');
                }, ['result', '', 'P"2!"/"']))
                    .catch(__assignType(err => {
                    reject(new MutexAcquisitionError(`Failed to renew mutex ${options.key} ${lockId}: ${String(err)}`));
                }, ['err', '', 'P"2!"/"']));
            }, options.renewInterval / 2);
            options
                .fn(attempts > 1)
                .then(resolve)
                .catch(reject)
                .finally(() => {
                clearInterval(interval);
                redisClient
                    .RELEASE(options.key, lockId)
                    .then(__assignType(result => {
                    if (result === 0)
                        throw new MutexAcquisitionError('Key missing or value mismatch');
                }, ['result', '', 'P"2!"/"']))
                    .catch(__assignType(err => {
                    console.warn(`Failed to release mutex ${options.key} ${lockId}`, err);
                }, ['err', '', 'P"2!"/"']));
            });
        }, ['resolve', 'reject', '', 'P"2!"2""/#'])));
    } while (Date.now() < deadlineMs);
    throw new MutexAcquisitionError();
}
redisMutexExec.__type = [() => __ΩIMutexOptions, 'options', 'redisMutexExec', 'P"o!"2""`/#'];
/**
 * Helpers
 */
function flattenMutexKey(key) {
    if (Array.isArray(key)) {
        return key.map(__assignType(k => flattenMutexKey(k), ['k', '', 'P"2!"/"'])).join(':');
    }
    if (typeof key === 'object') {
        if ('name' in key) {
            return key.name;
        }
        if ('constructor' in key) {
            return key.constructor.name;
        }
        const jsonKey = JSON.stringify(key);
        const objectKey = jsonKey.length === 2 ? String(key) : jsonKey;
        return (0, crypto_1.createHash)('md5').update(objectKey).digest('hex');
    }
    return String(key);
}
flattenMutexKey.__type = [() => __ΩMutexKey, () => __ΩMutexKey, 'key', 'flattenMutexKey', 'PPn!n"FJ2#&/$'];
//# sourceMappingURL=mutex.js.map