"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.__tsfTypeAliases = exports.WorkerRunnerService = void 0;
const tslib_1 = require("tslib");
const _a = tslib_1.__importStar(require("@zyno-io/ts-server-foundation/type-metadata-runtime"));
const helpers_1 = require("../../helpers");
const types_1 = require("./types");
const observer_1 = require("./observer");
const bullmq_1 = require("bullmq");
const __tsf_metadata_type = _a.createCompactMetadataRegistryV1("[1,[{\"kind\":22,\"type\":{\"kind\":3}},{\"kind\":3},{\"kind\":6},{\"kind\":18,\"typeName\":\"IJobOptions\",\"types\":[{\"kind\":20,\"name\":\"delay\",\"type\":{\"kind\":12,\"types\":[{\"kind\":7},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"queueName\",\"type\":{\"kind\":12,\"types\":[{\"kind\":6},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"runInTest\",\"type\":{\"kind\":12,\"types\":[{\"kind\":8},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"runImmediately\",\"type\":{\"kind\":12,\"types\":[{\"kind\":8},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"recordToDatabase\",\"type\":{\"kind\":12,\"types\":[{\"kind\":8},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"repeatKey\",\"type\":{\"kind\":12,\"types\":[{\"kind\":6},{\"kind\":4}]},\"optional\":true}]}]]", []);
const BULLMQ_REDIS_WARNING_AFTER_MS = 2000;
let _WorkerRunnerServiceMetadata_1;
class WorkerRunnerService {
    app;
    queueRegistry;
    recorder;
    logger;
    running = false;
    timers = new Map();
    executing = new Set();
    bullWorkers = new Map();
    bullWorkerRedisLifecycleCleanup = new Set();
    bullWorkerRedisMonitor;
    constructor(app, queueRegistry, recorder, logger) {
        this.app = app;
        this.queueRegistry = queueRegistry;
        this.recorder = recorder;
        this.logger = logger;
    }
    async start() {
        if (this.running)
            return;
        this.running = true;
        const registeredJobs = this.getRegisteredJobs();
        for (const jobClass of registeredJobs) {
            this.logger.info('Registering job', {
                job: {
                    name: jobClass.name,
                    queue: this.queueRegistry.getQueueName(jobClass),
                    schedule: jobClass.CRON_SCHEDULE
                }
            });
        }
        if (this.queueRegistry.usesBullMq()) {
            // A deployment migration may have run while an old worker was still active. Reconcile
            // again before this worker starts consuming jobs so a materialized stale repeat job
            // cannot reach this process and perpetuate its own chain.
            await this.removeStaleBullMqCronJobs();
            await this.startBullMqWorkers();
            await this.scheduleBullMqCronJobs();
            this.logger.info('Worker started', {
                backend: 'bullmq',
                queues: this.getRegisteredQueueNames()
            });
            return;
        }
        this.scheduleCronJobs();
        this.logger.info('Worker started', {
            backend: 'memory',
            queues: this.getRegisteredQueueNames()
        });
        await this.drainReadyJobs();
    }
    async shutdown() {
        const wasStarted = this.running || this.timers.size > 0 || this.bullWorkers.size > 0 || this.executing.size > 0;
        if (!wasStarted)
            return;
        this.running = false;
        this.logger.info('Worker stopping', {
            queues: this.getRegisteredQueueNames(),
            runningJobIds: [...this.executing]
        });
        for (const timer of this.timers.values())
            clearTimeout(timer);
        this.timers.clear();
        for (const cleanup of this.bullWorkerRedisLifecycleCleanup)
            cleanup();
        this.bullWorkerRedisLifecycleCleanup.clear();
        this.bullWorkerRedisMonitor?.stop();
        this.bullWorkerRedisMonitor = undefined;
        if (this.executing.size) {
            this.logger.warn('Waiting for jobs to finish', { jobIds: [...this.executing] });
        }
        let workerStopFailed = false;
        await Promise.all([...this.bullWorkers.entries()].map(async ([queue, worker]) => {
            try {
                await worker.close();
            }
            catch (error) {
                workerStopFailed = true;
                this.logger.error('Failed to stop worker', error, { queue });
            }
        }));
        this.bullWorkers.clear();
        while (this.executing.size) {
            await new Promise(resolve => setTimeout(resolve, 10));
        }
        if (!workerStopFailed)
            this.logger.info('Worker stopped');
    }
    async removeStaleBullMqCronJobs() {
        const desiredSchedules = [];
        for (const jobClass of (0, types_1.getRegisteredWorkerJobs)()) {
            const pattern = jobClass.CRON_SCHEDULE;
            if (!pattern || !this.isRegisteredJob(jobClass))
                continue;
            desiredSchedules.push({
                queue: this.queueRegistry.getQueueName(jobClass),
                name: jobClass.name,
                pattern
            });
        }
        const removed = await this.queueRegistry.removeStaleBullMqJobSchedulers(desiredSchedules);
        for (const scheduler of removed) {
            this.logger.info('Deregistered job during migration', { job: scheduler });
        }
    }
    schedule(job) {
        if (!this.running || job.status !== 'queued')
            return;
        const delay = Math.max(0, job.shouldExecuteAt.getTime() - Date.now());
        if (delay > 0) {
            if (this.timers.has(job.id))
                return;
            const timer = setTimeout(() => {
                this.timers.delete(job.id);
                this.runScheduledJob(job, false).catch(error => {
                    this.logger.error('Queued job failed', error, { job: getJobLogData(job) });
                });
            }, delay);
            this.timers.set(job.id, timer);
            return;
        }
        this.runScheduledJob(job, false).catch(error => {
            this.logger.error('Queued job failed', error, { job: getJobLogData(job) });
        });
    }
    async drainReadyJobs(queue) {
        if (this.queueRegistry.usesBullMq())
            return;
        const jobs = queue ? this.queueRegistry.getQueuedJobs(queue) : this.queueRegistry.getAllQueuedJobs();
        for (const job of jobs) {
            if (job.status !== 'queued')
                continue;
            if (job.shouldExecuteAt.getTime() > Date.now()) {
                this.schedule(job);
                continue;
            }
            await this.runScheduledJob(job);
        }
    }
    async executeQueuedJob(job, options = job.options) {
        return this.executeJob(job, options, true);
    }
    async executeJob(job, options, logFailure) {
        if (this.executing.has(job.id))
            throw new Error(`Job ${job.name}:${job.id} is already executing`);
        this.executing.add(job.id);
        job.attemptsMade++;
        job.data = this.queueRegistry.normalizeJobData(job.data);
        (0, observer_1.notifyWorkerObservers)({ type: 'active', job });
        this.logger.info('Job activated', { job: getJobLogData(job) });
        try {
            const result = await (0, helpers_1.withContextData)({
                job: {
                    queue: job.queue,
                    id: job.id,
                    name: job.name
                }
            }, async () => this.resolveJob(job.jobClass).handle(job.data));
            this.queueRegistry.markCompleted(job, result);
            await this.recorder.recordCompleted(job, result, options);
            this.logger.info('Job completed', { job: getJobLogData(job) });
            return { job, result: result };
        }
        catch (error) {
            const failure = formatFailure(error);
            this.queueRegistry.markFailed(job, failure);
            if (logFailure) {
                this.logger.error(`Job failed: ${failure.message}`, error, { job: getJobLogData(job) });
            }
            await this.recorder.recordFailed(job, failure, options);
            throw error;
        }
        finally {
            this.queueRegistry.remove(job);
            this.executing.delete(job.id);
        }
    }
    scheduleCronJobs() {
        for (const jobClass of (0, types_1.getRegisteredWorkerJobs)()) {
            const schedule = jobClass.CRON_SCHEDULE;
            if (!schedule || !this.isRegisteredJob(jobClass))
                continue;
            const repeatKey = `${jobClass.name}:${schedule}`;
            if (this.hasQueuedRepeatJob(repeatKey))
                continue;
            const job = this.queueRegistry.add(jobClass, {}, { delay: getCronDelayMs(schedule), repeatKey });
            this.schedule(job);
        }
    }
    rescheduleCronJob(job) {
        const repeatKey = job.options.repeatKey;
        const schedule = job.jobClass.CRON_SCHEDULE;
        if (!this.running || !repeatKey || !schedule)
            return;
        if (this.hasQueuedRepeatJob(repeatKey))
            return;
        const next = this.queueRegistry.add(job.jobClass, {}, { delay: getCronDelayMs(schedule), repeatKey });
        this.schedule(next);
    }
    async runScheduledJob(job, logFailure = true) {
        try {
            return await this.executeJob(job, job.options, logFailure);
        }
        finally {
            this.rescheduleCronJob(job);
        }
    }
    resolveJob(jobClass) {
        const registered = this.app.container.listRegisteredProviders().find(item => item.token === jobClass);
        if (!registered)
            throw new Error(`Job handler is not registered as a provider: ${jobClass.name}`);
        return this.app.container.resolve(jobClass, registered.moduleId);
    }
    isRegisteredJob(jobClass) {
        return this.app.container.listRegisteredProviders().some(item => item.token === jobClass);
    }
    hasQueuedRepeatJob(repeatKey) {
        return this.queueRegistry.getAllQueuedJobs().some(job => job.status === 'queued' && job.options.repeatKey === repeatKey);
    }
    async startBullMqWorkers() {
        this.bullWorkerRedisMonitor ??= (0, helpers_1.createAvailabilityMonitor)(this.logger, {
            alertAfterMs: this.app.config.REDIS_UNAVAILABLE_ALERT_AFTER_MS,
            name: 'BullMQ worker Redis',
            warningAfterMs: BULLMQ_REDIS_WARNING_AFTER_MS
        });
        for (const queueName of this.getRegisteredQueueNames()) {
            if (this.bullWorkers.has(queueName))
                continue;
            const worker = new bullmq_1.Worker(queueName, async (job) => {
                await this.executeBullMqJob(job);
            }, this.queueRegistry.getBullMqOptions());
            worker.on('failed', (job, error) => {
                this.logger.error(`Job failed: ${error.message}`, error, {
                    job: {
                        queue: queueName,
                        id: job?.id,
                        name: job?.name,
                        attempt: job?.attemptsMade
                    }
                });
            });
            worker.on('stalled', jobId => {
                this.logger.warn('Job stalled', { job: { queue: queueName, id: jobId } });
            });
            worker.on('error', error => {
                const timer = setTimeout(() => {
                    if (!this.running)
                        return;
                    if (isBullWorkerRedisReady(worker)) {
                        this.logger.error('BullMQ worker error', error, { queue: queueName });
                    }
                    else {
                        this.bullWorkerRedisMonitor?.unavailable(error);
                    }
                }, 0);
                timer.unref?.();
            });
            worker.once('ready', () => {
                this.logger.info('Worker ready', { queue: queueName });
            });
            this.bullWorkers.set(queueName, worker);
            this.bullWorkerRedisLifecycleCleanup.add(observeBullWorkerRedisLifecycle(worker, () => this.bullWorkerRedisMonitor?.unavailable(), () => {
                if ([...this.bullWorkers.values()].every(isBullWorkerRedisReady)) {
                    this.bullWorkerRedisMonitor?.available();
                }
            }));
        }
    }
    async scheduleBullMqCronJobs() {
        for (const jobClass of (0, types_1.getRegisteredWorkerJobs)()) {
            const schedule = jobClass.CRON_SCHEDULE;
            if (!schedule || !this.isRegisteredJob(jobClass))
                continue;
            const repeatKey = `${jobClass.name}:${schedule}`;
            const queue = this.queueRegistry.getBullQueue(this.queueRegistry.getQueueName(jobClass));
            await queue.upsertJobScheduler(repeatKey, { pattern: schedule }, {
                name: jobClass.name,
                data: {
                    data: {},
                    options: { repeatKey }
                },
                opts: {
                    removeOnComplete: 1000,
                    removeOnFail: 1000
                }
            });
        }
    }
    async executeBullMqJob(job) {
        const jobClass = this.findRegisteredJobClass(job.name);
        if (!jobClass) {
            if (job.repeatJobKey) {
                await this.queueRegistry.removeBullMqRepeatChain(job.queueName, job.repeatJobKey);
            }
            throw new Error(`Job handler is not registered as a provider: ${job.name}`);
        }
        const queuedJob = this.queueRegistry.fromBullJob(job, jobClass);
        return this.executeJob(queuedJob, queuedJob.options, false);
    }
    findRegisteredJobClass(name) {
        return (0, types_1.getRegisteredWorkerJobs)().find(candidate => candidate.name === name && this.isRegisteredJob(candidate));
    }
    getRegisteredQueueNames() {
        const queues = new Set([this.queueRegistry.getDefaultQueueName()]);
        for (const jobClass of (0, types_1.getRegisteredWorkerJobs)()) {
            if (this.isRegisteredJob(jobClass))
                queues.add(this.queueRegistry.getQueueName(jobClass));
        }
        return [...queues];
    }
    getRegisteredJobs() {
        return (0, types_1.getRegisteredWorkerJobs)().filter(jobClass => this.isRegisteredJob(jobClass));
    }
    static __tsfType = _WorkerRunnerServiceMetadata_1 = _a.decodeCompactMetadataV1("[1,{\"kind\":16,\"name\":\"WorkerRunnerService\",\"typeName\":\"WorkerRunnerService\",\"classType\":{\"$tsf\":0},\"properties\":[],\"methods\":[{\"name\":\"start\",\"parameters\":[],\"returnType\":{\"$tsfType\":0}},{\"name\":\"shutdown\",\"parameters\":[],\"returnType\":{\"$tsfType\":0}},{\"name\":\"removeStaleBullMqCronJobs\",\"parameters\":[],\"returnType\":{\"$tsfType\":0}},{\"name\":\"schedule\",\"parameters\":[{\"name\":\"job\",\"type\":{\"kind\":18,\"typeName\":\"QueuedWorkerJob\",\"types\":[{\"kind\":20,\"name\":\"id\",\"type\":{\"kind\":6},\"optional\":false},{\"kind\":20,\"name\":\"queue\",\"type\":{\"kind\":6},\"optional\":false},{\"kind\":20,\"name\":\"name\",\"type\":{\"kind\":6},\"optional\":false},{\"kind\":20,\"name\":\"data\",\"type\":{\"kind\":2,\"typeName\":\"I\"},\"optional\":false},{\"kind\":20,\"name\":\"jobClass\",\"type\":{\"kind\":2,\"typeName\":\"JobClass\",\"typeArguments\":[{\"kind\":2,\"typeName\":\"I\"}]},\"optional\":false},{\"kind\":20,\"name\":\"options\",\"type\":{\"kind\":18,\"typeName\":\"IJobOptions\",\"types\":[{\"kind\":20,\"name\":\"delay\",\"type\":{\"kind\":12,\"types\":[{\"kind\":7},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"queueName\",\"type\":{\"kind\":12,\"types\":[{\"kind\":6},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"runInTest\",\"type\":{\"kind\":12,\"types\":[{\"kind\":8},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"runImmediately\",\"type\":{\"kind\":12,\"types\":[{\"kind\":8},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"recordToDatabase\",\"type\":{\"kind\":12,\"types\":[{\"kind\":8},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"repeatKey\",\"type\":{\"kind\":12,\"types\":[{\"kind\":6},{\"kind\":4}]},\"optional\":true}]},\"optional\":false},{\"kind\":20,\"name\":\"createdAt\",\"type\":{\"kind\":16,\"typeName\":\"Date\",\"classType\":{\"$tsf\":1}},\"optional\":false},{\"kind\":20,\"name\":\"shouldExecuteAt\",\"type\":{\"kind\":16,\"typeName\":\"Date\",\"classType\":{\"$tsf\":1}},\"optional\":false},{\"kind\":20,\"name\":\"attemptsMade\",\"type\":{\"kind\":7},\"optional\":false},{\"kind\":20,\"name\":\"status\",\"type\":{\"kind\":12,\"types\":[{\"kind\":10,\"literal\":\"queued\"},{\"kind\":10,\"literal\":\"completed\"},{\"kind\":10,\"literal\":\"failed\"},{\"kind\":10,\"literal\":\"skipped\"}],\"typeName\":\"WorkerJobStatus\"},\"optional\":false},{\"kind\":20,\"name\":\"result\",\"type\":{\"kind\":12,\"types\":[{\"kind\":2},{\"kind\":4}]},\"optional\":true}],\"typeParameters\":[\"I\"]},\"optional\":false,\"default\":false}],\"returnType\":{\"$tsfType\":1}},{\"name\":\"drainReadyJobs\",\"parameters\":[{\"name\":\"queue\",\"type\":{\"$tsfType\":2},\"optional\":true,\"default\":false}],\"returnType\":{\"$tsfType\":0}},{\"name\":\"executeQueuedJob\",\"parameters\":[{\"name\":\"job\",\"type\":{\"kind\":18,\"typeName\":\"QueuedWorkerJob\",\"types\":[{\"kind\":20,\"name\":\"id\",\"type\":{\"kind\":6},\"optional\":false},{\"kind\":20,\"name\":\"queue\",\"type\":{\"kind\":6},\"optional\":false},{\"kind\":20,\"name\":\"name\",\"type\":{\"kind\":6},\"optional\":false},{\"kind\":20,\"name\":\"data\",\"type\":{\"kind\":2,\"typeName\":\"I\"},\"optional\":false},{\"kind\":20,\"name\":\"jobClass\",\"type\":{\"kind\":2,\"typeName\":\"JobClass\",\"typeArguments\":[{\"kind\":2,\"typeName\":\"I\"}]},\"optional\":false},{\"kind\":20,\"name\":\"options\",\"type\":{\"kind\":18,\"typeName\":\"IJobOptions\",\"types\":[{\"kind\":20,\"name\":\"delay\",\"type\":{\"kind\":12,\"types\":[{\"kind\":7},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"queueName\",\"type\":{\"kind\":12,\"types\":[{\"kind\":6},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"runInTest\",\"type\":{\"kind\":12,\"types\":[{\"kind\":8},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"runImmediately\",\"type\":{\"kind\":12,\"types\":[{\"kind\":8},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"recordToDatabase\",\"type\":{\"kind\":12,\"types\":[{\"kind\":8},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"repeatKey\",\"type\":{\"kind\":12,\"types\":[{\"kind\":6},{\"kind\":4}]},\"optional\":true}]},\"optional\":false},{\"kind\":20,\"name\":\"createdAt\",\"type\":{\"kind\":16,\"typeName\":\"Date\",\"classType\":{\"$tsf\":1}},\"optional\":false},{\"kind\":20,\"name\":\"shouldExecuteAt\",\"type\":{\"kind\":16,\"typeName\":\"Date\",\"classType\":{\"$tsf\":1}},\"optional\":false},{\"kind\":20,\"name\":\"attemptsMade\",\"type\":{\"kind\":7},\"optional\":false},{\"kind\":20,\"name\":\"status\",\"type\":{\"kind\":12,\"types\":[{\"kind\":10,\"literal\":\"queued\"},{\"kind\":10,\"literal\":\"completed\"},{\"kind\":10,\"literal\":\"failed\"},{\"kind\":10,\"literal\":\"skipped\"}],\"typeName\":\"WorkerJobStatus\"},\"optional\":false},{\"kind\":20,\"name\":\"result\",\"type\":{\"kind\":12,\"types\":[{\"kind\":2},{\"kind\":4}]},\"optional\":true}]},\"optional\":false,\"default\":false},{\"name\":\"options\",\"type\":{\"$tsfType\":3},\"optional\":true,\"default\":true}],\"returnType\":{\"kind\":22,\"type\":{\"kind\":18,\"typeName\":\"WorkerExecutionResult\",\"types\":[{\"kind\":20,\"name\":\"job\",\"type\":{\"kind\":18,\"typeName\":\"QueuedWorkerJob\",\"types\":[{\"kind\":20,\"name\":\"id\",\"type\":{\"kind\":6},\"optional\":false},{\"kind\":20,\"name\":\"queue\",\"type\":{\"kind\":6},\"optional\":false},{\"kind\":20,\"name\":\"name\",\"type\":{\"kind\":6},\"optional\":false},{\"kind\":20,\"name\":\"data\",\"type\":{\"kind\":2,\"typeName\":\"I\"},\"optional\":false},{\"kind\":20,\"name\":\"jobClass\",\"type\":{\"kind\":2,\"typeName\":\"JobClass\",\"typeArguments\":[{\"kind\":2,\"typeName\":\"I\"}]},\"optional\":false},{\"kind\":20,\"name\":\"options\",\"type\":{\"kind\":18,\"typeName\":\"IJobOptions\",\"types\":[{\"kind\":20,\"name\":\"delay\",\"type\":{\"kind\":12,\"types\":[{\"kind\":7},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"queueName\",\"type\":{\"kind\":12,\"types\":[{\"kind\":6},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"runInTest\",\"type\":{\"kind\":12,\"types\":[{\"kind\":8},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"runImmediately\",\"type\":{\"kind\":12,\"types\":[{\"kind\":8},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"recordToDatabase\",\"type\":{\"kind\":12,\"types\":[{\"kind\":8},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"repeatKey\",\"type\":{\"kind\":12,\"types\":[{\"kind\":6},{\"kind\":4}]},\"optional\":true}]},\"optional\":false},{\"kind\":20,\"name\":\"createdAt\",\"type\":{\"kind\":16,\"typeName\":\"Date\",\"classType\":{\"$tsf\":1}},\"optional\":false},{\"kind\":20,\"name\":\"shouldExecuteAt\",\"type\":{\"kind\":16,\"typeName\":\"Date\",\"classType\":{\"$tsf\":1}},\"optional\":false},{\"kind\":20,\"name\":\"attemptsMade\",\"type\":{\"kind\":7},\"optional\":false},{\"kind\":20,\"name\":\"status\",\"type\":{\"kind\":12,\"types\":[{\"kind\":10,\"literal\":\"queued\"},{\"kind\":10,\"literal\":\"completed\"},{\"kind\":10,\"literal\":\"failed\"},{\"kind\":10,\"literal\":\"skipped\"}],\"typeName\":\"WorkerJobStatus\"},\"optional\":false},{\"kind\":20,\"name\":\"result\",\"type\":{\"kind\":12,\"types\":[{\"kind\":2},{\"kind\":4}]},\"optional\":true}]},\"optional\":false},{\"kind\":20,\"name\":\"result\",\"type\":{\"kind\":2,\"typeName\":\"O\"},\"optional\":false}]}}}],\"hasConstructor\":true,\"constructorParameters\":[{\"name\":\"app\",\"type\":{\"kind\":16,\"typeName\":\"App\",\"classType\":{\"$tsfImport\":[2,\"../../app/index\",\"App\"]}},\"optional\":false,\"default\":false},{\"name\":\"queueRegistry\",\"type\":{\"kind\":16,\"typeName\":\"WorkerQueueRegistry\",\"classType\":{\"$tsfImport\":[2,\"./queue\",\"WorkerQueueRegistry\"]}},\"optional\":false,\"default\":false},{\"name\":\"recorder\",\"type\":{\"kind\":16,\"typeName\":\"WorkerRecorderService\",\"classType\":{\"$tsfImport\":[2,\"./recorder\",\"WorkerRecorderService\"]}},\"optional\":false,\"default\":false},{\"name\":\"logger\",\"type\":{\"kind\":16,\"typeName\":\"ScopedLogger\",\"classType\":{\"$tsfImport\":[2,\"../logger\",\"ScopedLogger\"]}},\"optional\":false,\"default\":false}]}]", [() => WorkerRunnerService, () => (typeof Date !== "undefined" ? Date : (typeof exports !== "undefined" ? exports.Date : undefined)), require], __tsf_metadata_type);
}
exports.WorkerRunnerService = WorkerRunnerService;
_WorkerRunnerServiceMetadata_1.classType = WorkerRunnerService;
WorkerRunnerService.__tsfType = _WorkerRunnerServiceMetadata_1;
function getBullWorkerRedisClients(worker) {
    // RedisConnection.status remains "ready" while its ioredis client reconnects. Read the actual
    // command and blocking clients so failover lifecycle events and recovery are both observable.
    const internal = worker;
    return [internal.connection?._client, internal.blockingConnection?._client].filter((client) => client !== undefined);
}
function isBullWorkerRedisReady(worker) {
    const clients = getBullWorkerRedisClients(worker);
    return clients.length === 2 && clients.every(client => client.status === 'ready');
}
function observeBullWorkerRedisLifecycle(worker, onUnavailable, onReady) {
    const clients = getBullWorkerRedisClients(worker);
    for (const client of clients) {
        client.on('reconnecting', onUnavailable);
        client.on('ready', onReady);
    }
    return () => {
        for (const client of clients) {
            client.removeListener('reconnecting', onUnavailable);
            client.removeListener('ready', onReady);
        }
    };
}
function formatFailure(error) {
    if (error instanceof Error)
        return { message: error.message, stack: error.stack };
    return { message: String(error) };
}
function getJobLogData(job) {
    return {
        queue: job.queue,
        id: job.id,
        name: job.name,
        attempt: job.attemptsMade
    };
}
function getCronDelayMs(schedule) {
    const parts = schedule.trim().split(/\s+/);
    if (parts.length !== 5 && parts.length !== 6)
        throw new Error(`Invalid cron schedule "${schedule}"`);
    const fields = parts.length === 5 ? ['0', ...parts] : parts;
    const cron = {
        seconds: parseCronField(fields[0], 0, 59),
        minutes: parseCronField(fields[1], 0, 59),
        hours: parseCronField(fields[2], 0, 23),
        daysOfMonth: parseCronField(fields[3], 1, 31),
        months: parseCronField(fields[4], 1, 12),
        daysOfWeek: uniqueSorted(parseCronField(fields[5], 0, 7).map(value => (value === 7 ? 0 : value)))
    };
    const now = new Date();
    const next = nextCronDate(cron, now);
    return Math.max(1, next.getTime() - now.getTime());
}
function nextCronDate(cron, now) {
    const candidate = new Date(now.getTime() + 1000);
    candidate.setMilliseconds(0);
    const maxMinutes = 366 * 5 * 24 * 60;
    for (let attempt = 0; attempt < maxMinutes; attempt++) {
        if (matchesCronMinute(candidate, cron)) {
            const second = cron.seconds.find(value => value >= candidate.getSeconds());
            if (second !== undefined) {
                const next = new Date(candidate);
                next.setSeconds(second, 0);
                if (next > now)
                    return next;
            }
        }
        candidate.setMinutes(candidate.getMinutes() + 1, 0, 0);
    }
    throw new Error('Could not calculate next cron execution within five years');
}
function matchesCronMinute(date, cron) {
    if (!cron.minutes.includes(date.getMinutes()))
        return false;
    if (!cron.hours.includes(date.getHours()))
        return false;
    if (!cron.months.includes(date.getMonth() + 1))
        return false;
    const dayOfMonthWildcard = cron.daysOfMonth.length === 31;
    const dayOfWeekWildcard = cron.daysOfWeek.length === 7;
    const dayOfMonthMatches = cron.daysOfMonth.includes(date.getDate());
    const dayOfWeekMatches = cron.daysOfWeek.includes(date.getDay());
    if (dayOfMonthWildcard && dayOfWeekWildcard)
        return true;
    if (dayOfMonthWildcard)
        return dayOfWeekMatches;
    if (dayOfWeekWildcard)
        return dayOfMonthMatches;
    return dayOfMonthMatches || dayOfWeekMatches;
}
function parseCronField(expression, min, max) {
    const values = new Set();
    for (const part of expression.split(',')) {
        addCronPart(values, part.trim(), min, max);
    }
    return [...values].sort((a, b) => a - b);
}
function uniqueSorted(values) {
    return [...new Set(values)].sort((a, b) => a - b);
}
function addCronPart(values, expression, min, max) {
    if (!expression)
        throw new Error('Invalid empty cron field');
    const [rangeExpression, stepExpression] = expression.split('/');
    if (stepExpression !== undefined && !/^\d+$/.test(stepExpression))
        throw new Error(`Invalid cron step "${expression}"`);
    const step = stepExpression === undefined ? 1 : Number(stepExpression);
    if (!Number.isInteger(step) || step < 1)
        throw new Error(`Invalid cron step "${expression}"`);
    const [start, end] = parseCronRange(rangeExpression, min, max);
    for (let value = start; value <= end; value += step) {
        if (value < min || value > max)
            throw new Error(`Cron value ${value} is outside ${min}-${max}`);
        values.add(value);
    }
}
function parseCronRange(expression, min, max) {
    if (expression === '*')
        return [min, max];
    const range = expression.match(/^(\d+)-(\d+)$/);
    if (range) {
        const start = Number(range[1]);
        const end = Number(range[2]);
        if (start > end)
            throw new Error(`Invalid cron range "${expression}"`);
        return [start, end];
    }
    if (/^\d+$/.test(expression)) {
        const value = Number(expression);
        return [value, value];
    }
    throw new Error(`Unsupported cron field "${expression}"`);
}
exports.__tsfTypeAliases = _a.decodeCompactMetadataV1("[1,{\"WorkerExecutionResult\":{\"kind\":18,\"typeName\":\"WorkerExecutionResult\",\"types\":[{\"kind\":20,\"name\":\"job\",\"type\":{\"kind\":18,\"typeName\":\"QueuedWorkerJob\",\"types\":[{\"kind\":20,\"name\":\"id\",\"type\":{\"kind\":6},\"optional\":false},{\"kind\":20,\"name\":\"queue\",\"type\":{\"kind\":6},\"optional\":false},{\"kind\":20,\"name\":\"name\",\"type\":{\"kind\":6},\"optional\":false},{\"kind\":20,\"name\":\"data\",\"type\":{\"kind\":2,\"typeName\":\"I\"},\"optional\":false},{\"kind\":20,\"name\":\"jobClass\",\"type\":{\"kind\":2,\"typeName\":\"JobClass\",\"typeArguments\":[{\"kind\":2,\"typeName\":\"I\"}]},\"optional\":false},{\"kind\":20,\"name\":\"options\",\"type\":{\"kind\":18,\"typeName\":\"IJobOptions\",\"types\":[{\"kind\":20,\"name\":\"delay\",\"type\":{\"kind\":12,\"types\":[{\"kind\":7},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"queueName\",\"type\":{\"kind\":12,\"types\":[{\"kind\":6},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"runInTest\",\"type\":{\"kind\":12,\"types\":[{\"kind\":8},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"runImmediately\",\"type\":{\"kind\":12,\"types\":[{\"kind\":8},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"recordToDatabase\",\"type\":{\"kind\":12,\"types\":[{\"kind\":8},{\"kind\":4}]},\"optional\":true},{\"kind\":20,\"name\":\"repeatKey\",\"type\":{\"kind\":12,\"types\":[{\"kind\":6},{\"kind\":4}]},\"optional\":true}]},\"optional\":false},{\"kind\":20,\"name\":\"createdAt\",\"type\":{\"kind\":16,\"typeName\":\"Date\",\"classType\":{\"$tsf\":0}},\"optional\":false},{\"kind\":20,\"name\":\"shouldExecuteAt\",\"type\":{\"kind\":16,\"typeName\":\"Date\",\"classType\":{\"$tsf\":0}},\"optional\":false},{\"kind\":20,\"name\":\"attemptsMade\",\"type\":{\"kind\":7},\"optional\":false},{\"kind\":20,\"name\":\"status\",\"type\":{\"kind\":12,\"types\":[{\"kind\":10,\"literal\":\"queued\"},{\"kind\":10,\"literal\":\"completed\"},{\"kind\":10,\"literal\":\"failed\"},{\"kind\":10,\"literal\":\"skipped\"}],\"typeName\":\"WorkerJobStatus\"},\"optional\":false},{\"kind\":20,\"name\":\"result\",\"type\":{\"kind\":12,\"types\":[{\"kind\":2},{\"kind\":4}]},\"optional\":true}]},\"optional\":false},{\"kind\":20,\"name\":\"result\",\"type\":{\"kind\":2,\"typeName\":\"O\"},\"optional\":false}],\"typeParameters\":[\"I\",\"O\"]}}]", [() => (typeof Date !== "undefined" ? Date : (typeof exports !== "undefined" ? exports.Date : undefined))], __tsf_metadata_type);
//# sourceMappingURL=runner.js.map