"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.__tsfTypeAliases = exports.MutexAcquisitionError = void 0;
exports.withMutex = withMutex;
exports.registerMutexObserver = registerMutexObserver;
exports.withMutexes = withMutexes;
exports.flattenMutexKey = flattenMutexKey;
exports.resetMutexRedisConnection = resetMutexRedisConnection;
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 date_1 = require("../utils/date");
const lifecycle_1 = require("./lifecycle");
const redis_1 = require("./redis");
const __tsf_metadata_type = _a.createCompactMetadataRegistryV1("[1,[{\"kind\":2}]]", []);
const DefaultMutexOptions = {
    retryCount: 30,
    retryDelay: 1000,
    renewInterval: 1000
};
const localMutexes = new Map();
let defaultRedisConnection;
const mutexObservers = new Set();
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 _MutexAcquisitionErrorMetadata_1;
class MutexAcquisitionError extends Error {
    constructor(message = 'Failed to acquire mutex within timeout') {
        super(message);
        this.name = 'MutexAcquisitionError';
    }
    static __tsfType = _MutexAcquisitionErrorMetadata_1 = _a.decodeCompactMetadataV1("[1,{\"kind\":16,\"name\":\"MutexAcquisitionError\",\"typeName\":\"MutexAcquisitionError\",\"classType\":{\"$tsf\":0},\"properties\":[],\"methods\":[],\"hasConstructor\":true,\"constructorParameters\":[{\"name\":\"message\",\"type\":{\"$tsfType\":0},\"optional\":true,\"default\":true}]}]", [() => MutexAcquisitionError], __tsf_metadata_type);
}
exports.MutexAcquisitionError = MutexAcquisitionError;
_MutexAcquisitionErrorMetadata_1.classType = MutexAcquisitionError;
MutexAcquisitionError.__tsfType = _MutexAcquisitionErrorMetadata_1;
async function withMutex(options) {
    const resolved = { ...DefaultMutexOptions, ...options };
    const key = flattenMutexKey(resolved.key);
    const mode = resolved.mode ?? getConfiguredMutexMode();
    if (mode === 'redis')
        return redisMutexExec(key, resolved);
    return localMutexExec(key, resolved);
}
function registerMutexObserver(observer) {
    mutexObservers.add(observer);
    return () => mutexObservers.delete(observer);
}
function withMutexes(options) {
    return withMutexesInternal({ ...options, keys: orderMutexKeys(options.keys) }, 0, false, []);
}
function withMutexesInternal(options, index, didAnyWait, contexts) {
    const currentKey = options.keys[index];
    const parentContext = createCompositeMutexContext(contexts, options.signal);
    parentContext.assertNotAborted();
    if (currentKey === undefined)
        return options.fn(didAnyWait, parentContext);
    return withMutex({
        ...options,
        key: currentKey,
        signal: parentContext.signal,
        fn: (didWait, context) => {
            return withMutexesInternal(options, index + 1, didAnyWait || didWait, [...contexts, context]);
        }
    });
}
function flattenMutexKey(key) {
    if (Array.isArray(key))
        return key.map(part => flattenMutexKey(part)).join(':');
    if (key === null)
        return 'null';
    if (typeof key === 'object') {
        if ('name' in key && typeof key.name === 'string')
            return key.name;
        const constructorName = key.constructor?.name;
        if (constructorName && constructorName !== 'Object')
            return constructorName;
        const jsonKey = JSON.stringify(key);
        const objectKey = jsonKey && jsonKey !== '{}' ? jsonKey : String(key);
        return (0, node_crypto_1.createHash)('md5').update(objectKey).digest('hex');
    }
    return String(key);
}
async function localMutexExec(key, options) {
    const observationId = (0, node_crypto_1.randomUUID)();
    const startedAt = Date.now();
    const deadline = Date.now() + options.retryCount * options.retryDelay;
    let didWait = false;
    let acquired = false;
    notifyMutexObservers({
        id: observationId,
        key,
        mode: 'local',
        type: 'pending',
        startedAt,
        at: startedAt
    });
    try {
        while (localMutexes.has(key)) {
            throwIfSignalAborted(options.signal, key);
            didWait = true;
            const current = localMutexes.get(key);
            const remaining = deadline - Date.now();
            if (remaining <= 0)
                throw new MutexAcquisitionError();
            await Promise.race([current.catch(() => { }), sleepOrAbort(Math.min(options.retryDelay, remaining), options.signal, key)]);
        }
        throwIfSignalAborted(options.signal, key);
    }
    catch (error) {
        notifyMutexObservers({
            id: observationId,
            key,
            mode: 'local',
            type: 'failed',
            startedAt,
            at: Date.now(),
            waited: didWait,
            error
        });
        throw error;
    }
    let release;
    const lock = new Promise(resolve => {
        release = resolve;
    });
    localMutexes.set(key, lock);
    acquired = true;
    notifyMutexObservers({
        id: observationId,
        key,
        mode: 'local',
        type: 'acquired',
        startedAt,
        at: Date.now(),
        waited: didWait
    });
    try {
        const context = createMutexContext(key, undefined, new AbortController(), options.signal);
        context.assertNotAborted();
        return await options.fn(didWait, context);
    }
    catch (error) {
        notifyMutexObservers({
            id: observationId,
            key,
            mode: 'local',
            type: 'error',
            startedAt,
            at: Date.now(),
            waited: didWait,
            error
        });
        throw error;
    }
    finally {
        if (localMutexes.get(key) === lock) {
            localMutexes.delete(key);
            release();
        }
        if (acquired)
            notifyMutexObservers({
                id: observationId,
                key,
                mode: 'local',
                type: 'released',
                startedAt,
                at: Date.now(),
                waited: didWait
            });
    }
}
async function redisMutexExec(key, options) {
    const observationId = (0, node_crypto_1.randomUUID)();
    const startedAt = Date.now();
    const lockId = (0, node_crypto_1.randomUUID)();
    const { client, prefix } = (options.redis ?? getDefaultRedisConnection)();
    const redisKey = `${prefix}:${key}`;
    const lockTtl = options.renewInterval * 3;
    const maxAttempts = Math.max(1, options.retryCount);
    let acquired = false;
    notifyMutexObservers({
        id: observationId,
        key: redisKey,
        mode: 'redis',
        type: 'pending',
        startedAt,
        at: startedAt
    });
    try {
        for (let attempts = 1; attempts <= maxAttempts; attempts++) {
            throwIfSignalAborted(options.signal, redisKey);
            if (Number(await evalRedisScript(client, ACQUIRE_SCRIPT, redisKey, lockId, lockTtl)) === 1) {
                acquired = true;
                notifyMutexObservers({
                    id: observationId,
                    key: redisKey,
                    mode: 'redis',
                    type: 'acquired',
                    startedAt,
                    at: Date.now(),
                    waited: attempts > 1
                });
                const result = await runWithRedisLock(client, redisKey, lockId, lockTtl, options.renewInterval, context => options.fn(attempts > 1, context), options.signal);
                notifyMutexObservers({
                    id: observationId,
                    key: redisKey,
                    mode: 'redis',
                    type: 'released',
                    startedAt,
                    at: Date.now(),
                    waited: attempts > 1
                });
                return result;
            }
            if (attempts < maxAttempts) {
                await sleepOrAbort(options.retryDelay, options.signal, redisKey);
            }
        }
        throw new MutexAcquisitionError();
    }
    catch (error) {
        notifyMutexObservers({
            id: observationId,
            key: redisKey,
            mode: 'redis',
            type: acquired ? 'error' : 'failed',
            startedAt,
            at: Date.now(),
            error
        });
        throw error;
    }
}
async function runWithRedisLock(client, key, lockId, lockTtl, renewInterval, fn, parentSignal) {
    throwIfSignalAborted(parentSignal, key);
    const abortController = new AbortController();
    let rejectRenewal;
    const renewalFailure = new Promise((_, reject) => {
        rejectRenewal = reject;
    });
    const context = createMutexContext(key, lockId, abortController);
    let lockFailed = false;
    let interval;
    const failLock = (error) => {
        if (lockFailed)
            return;
        lockFailed = true;
        if (interval)
            clearInterval(interval);
        rejectRenewal(error);
        abortController.abort(error);
    };
    const abortFromParent = () => failLock(getAbortError(parentSignal, key));
    parentSignal?.addEventListener('abort', abortFromParent, { once: true });
    interval = setInterval(() => {
        evalRedisScript(client, RENEW_SCRIPT, key, lockId, lockTtl)
            .then(result => {
            if (Number(result) !== 1)
                failLock(new MutexAcquisitionError(`Key missing or value mismatch for ${key}`));
        })
            .catch(error => failLock(new MutexAcquisitionError(`Failed to renew mutex ${key} ${lockId}: ${String(error)}`)));
    }, Math.max(1, Math.floor(renewInterval / 2)));
    const callback = Promise.resolve().then(() => {
        context.assertNotAborted();
        return fn(context);
    });
    let lockError;
    let result;
    let errorToThrow;
    try {
        result = await Promise.race([
            callback,
            renewalFailure.catch(error => {
                lockError = error;
                throw error;
            })
        ]);
    }
    catch (error) {
        if (lockError !== undefined)
            await callback.catch(() => { });
        errorToThrow = error;
    }
    finally {
        parentSignal?.removeEventListener('abort', abortFromParent);
        if (interval)
            clearInterval(interval);
        const releaseResult = await evalRedisScript(client, RELEASE_SCRIPT, key, lockId).catch(error => {
            console.warn(`Failed to release mutex ${key} ${lockId}`, error);
            return undefined;
        });
        if (releaseResult !== undefined && Number(releaseResult) !== 1 && !lockFailed && errorToThrow === undefined) {
            errorToThrow = new MutexAcquisitionError(`Failed to release mutex ${key} ${lockId}: key missing or value mismatch`);
        }
    }
    if (errorToThrow !== undefined)
        throw errorToThrow;
    return result;
}
function createMutexContext(key, lockId, abortController = new AbortController(), parentSignal) {
    if (parentSignal?.aborted)
        abortController.abort(parentSignal.reason);
    else
        parentSignal?.addEventListener('abort', () => abortController.abort(parentSignal.reason), {
            once: true
        });
    return {
        key,
        lockId,
        signal: abortController.signal,
        assertNotAborted() {
            if (!abortController.signal.aborted)
                return;
            const reason = abortController.signal.reason;
            throw reason instanceof Error ? reason : new MutexAcquisitionError(`Mutex ${key} was aborted`);
        }
    };
}
function createCompositeMutexContext(contexts, parentSignal) {
    if (contexts.length === 0)
        return createMutexContext('', undefined, new AbortController(), parentSignal);
    if (contexts.length === 1 && !parentSignal)
        return contexts[0];
    const abortController = new AbortController();
    for (const context of contexts) {
        if (context.signal.aborted) {
            abortCompositeMutexContext(abortController, context);
            break;
        }
        context.signal.addEventListener('abort', () => abortCompositeMutexContext(abortController, context), { once: true });
    }
    if (parentSignal?.aborted)
        abortController.abort(parentSignal.reason);
    else
        parentSignal?.addEventListener('abort', () => abortController.abort(parentSignal.reason), {
            once: true
        });
    return {
        key: contexts.map(context => context.key).join(','),
        signal: abortController.signal,
        assertNotAborted() {
            for (const context of contexts)
                context.assertNotAborted();
            throwIfSignalAborted(parentSignal, contexts.map(context => context.key).join(','));
            if (!abortController.signal.aborted)
                return;
            const reason = abortController.signal.reason;
            throw reason instanceof Error ? reason : new MutexAcquisitionError('Mutex group was aborted');
        }
    };
}
function abortCompositeMutexContext(abortController, context) {
    if (abortController.signal.aborted)
        return;
    abortController.abort(context.signal.reason);
}
function orderMutexKeys(keys) {
    const seen = new Set();
    return [...keys]
        .map(key => ({ key, flattened: flattenMutexKey(key) }))
        .sort((a, b) => a.flattened.localeCompare(b.flattened))
        .filter(({ flattened }) => {
        if (seen.has(flattened))
            return false;
        seen.add(flattened);
        return true;
    })
        .map(({ key }) => key);
}
function evalRedisScript(client, script, key, ...args) {
    return Promise.resolve().then(() => client.eval(script, 1, key, ...args));
}
function sleepOrAbort(ms, signal, key) {
    if (!signal)
        return (0, date_1.sleepMs)(ms);
    return new Promise((resolve, reject) => {
        if (signal.aborted) {
            reject(getAbortError(signal, key));
            return;
        }
        const cleanup = () => signal.removeEventListener('abort', handleAbort);
        const timeout = setTimeout(() => {
            cleanup();
            resolve();
        }, ms);
        const handleAbort = () => {
            clearTimeout(timeout);
            cleanup();
            reject(getAbortError(signal, key));
        };
        signal.addEventListener('abort', handleAbort, { once: true });
    });
}
function throwIfSignalAborted(signal, key) {
    if (!signal?.aborted)
        return;
    throw getAbortError(signal, key);
}
function getAbortError(signal, key) {
    const reason = signal?.reason;
    return reason instanceof Error ? reason : new MutexAcquisitionError(`Mutex ${key} was aborted`);
}
function getDefaultRedisConnection() {
    if (!defaultRedisConnection) {
        const connection = (0, redis_1.createRedis)('MUTEX');
        (0, lifecycle_1.registerRedisStateReset)(connection.client, () => {
            if (defaultRedisConnection === connection)
                defaultRedisConnection = undefined;
        });
        defaultRedisConnection = connection;
    }
    return defaultRedisConnection;
}
function resetMutexRedisConnection() {
    defaultRedisConnection = undefined;
}
function notifyMutexObservers(entry) {
    for (const observer of mutexObservers) {
        try {
            observer(entry);
        }
        catch {
            // Observers must never affect mutex behavior.
        }
    }
}
function getConfiguredMutexMode() {
    try {
        return (0, resolver_1.getAppConfig)().MUTEX_MODE ?? 'local';
    }
    catch {
        return 'local';
    }
}
exports.__tsfTypeAliases = _a.decodeCompactMetadataV1("[1,{\"MutexContext\":{\"kind\":18,\"typeName\":\"MutexContext\",\"types\":[{\"kind\":20,\"name\":\"key\",\"type\":{\"kind\":6},\"optional\":false},{\"kind\":20,\"name\":\"lockId\",\"type\":{\"kind\":12,\"types\":[{\"kind\":6},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"signal\",\"type\":{\"kind\":16,\"typeName\":\"AbortSignal\",\"classType\":{\"$tsf\":0}},\"optional\":false}]},\"MutexKey\":{\"kind\":1,\"typeName\":\"MutexKey\"},\"MutexObservation\":{\"kind\":18,\"typeName\":\"MutexObservation\",\"types\":[{\"kind\":20,\"name\":\"id\",\"type\":{\"kind\":6},\"optional\":false},{\"kind\":20,\"name\":\"key\",\"type\":{\"kind\":6},\"optional\":false},{\"kind\":20,\"name\":\"mode\",\"type\":{\"kind\":12,\"types\":[{\"kind\":10,\"literal\":\"local\"},{\"kind\":10,\"literal\":\"redis\"}]},\"optional\":false},{\"kind\":20,\"name\":\"type\",\"type\":{\"kind\":12,\"types\":[{\"kind\":10,\"literal\":\"pending\"},{\"kind\":10,\"literal\":\"acquired\"},{\"kind\":10,\"literal\":\"released\"},{\"kind\":10,\"literal\":\"error\"},{\"kind\":10,\"literal\":\"failed\"}]},\"optional\":false},{\"kind\":20,\"name\":\"startedAt\",\"type\":{\"kind\":7},\"optional\":false},{\"kind\":20,\"name\":\"at\",\"type\":{\"kind\":7},\"optional\":false},{\"kind\":20,\"name\":\"waited\",\"type\":{\"kind\":12,\"types\":[{\"kind\":8},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"error\",\"type\":{\"kind\":12,\"types\":[{\"kind\":2},{\"kind\":4}]},\"optional\":true}]},\"MutexObserver\":{\"kind\":2,\"typeName\":\"(entry: MutexObservation) =\\u003e void\",\"typeName\":\"MutexObserver\"},\"MutexOptions\":{\"kind\":18,\"typeName\":\"MutexOptions\",\"types\":[{\"kind\":20,\"name\":\"key\",\"type\":{\"kind\":12,\"types\":[{\"kind\":1,\"typeName\":\"MutexKey\"},{\"kind\":14,\"type\":{\"kind\":1,\"typeName\":\"MutexKey\"}}]},\"optional\":false},{\"kind\":20,\"name\":\"fn\",\"type\":{\"kind\":2,\"typeName\":\"MutexFn\",\"typeArguments\":[{\"kind\":2,\"typeName\":\"T\"}]},\"optional\":false},{\"kind\":20,\"name\":\"retryCount\",\"type\":{\"kind\":12,\"types\":[{\"kind\":7},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"retryDelay\",\"type\":{\"kind\":12,\"types\":[{\"kind\":7},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"renewInterval\",\"type\":{\"kind\":12,\"types\":[{\"kind\":7},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"mode\",\"type\":{\"kind\":12,\"types\":[{\"kind\":12,\"types\":[{\"kind\":10,\"literal\":\"local\"},{\"kind\":10,\"literal\":\"redis\"}]},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"redis\",\"type\":{\"kind\":12,\"types\":[{\"kind\":2,\"typeName\":\"() =\\u003e RedisConnection\\u003cRedisMutexClient\\u003e\",\"typeName\":\"RedisMutexProvider\"},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"signal\",\"type\":{\"kind\":12,\"types\":[{\"kind\":16,\"typeName\":\"AbortSignal\",\"classType\":{\"$tsf\":0}},{\"kind\":4}]},\"optional\":true}],\"typeParameters\":[\"T\"]},\"RedisMutexClient\":{\"kind\":18,\"typeName\":\"RedisMutexClient\",\"types\":[]},\"RedisMutexProvider\":{\"kind\":2,\"typeName\":\"() =\\u003e RedisConnection\\u003cRedisMutexClient\\u003e\",\"typeName\":\"RedisMutexProvider\"}}]", [() => (typeof AbortSignal !== "undefined" ? AbortSignal : (typeof exports !== "undefined" ? exports.AbortSignal : undefined))], __tsf_metadata_type);
//# sourceMappingURL=mutex.js.map