UNPKG

@sidequest/engine

Version:

@sidequest/engine is the core engine of SideQuest, a distributed background job processing system for Node.js and TypeScript.

248 lines (244 loc) 9.98 kB
'use strict'; var core = require('@sidequest/core'); var nodeCron = require('node-cron'); var node_util = require('node:util'); require('node:fs'); require('node:url'); var manualLoader = require('../shared-runner/manual-loader.cjs'); require('piscina'); require('../constants.cjs'); var constants = require('./constants.cjs'); var cronRegistry = require('./cron-registry.cjs'); /** * Builder for creating and enqueuing jobs with custom configuration. * @template T The job class type. */ class JobBuilder { backend; JobClass; defaults; manualJobResolution; constructorArgs; queueName; jobTimeout; uniquenessConfig; jobMaxAttempts; jobAvailableAt; jobRetryDelay; jobBackoffStrategy; /** * Creates a new JobBuilder for the given job class. * @param JobClass The job class constructor. */ constructor(backend, JobClass, defaults, manualJobResolution = false) { this.backend = backend; this.JobClass = JobClass; this.defaults = defaults; this.manualJobResolution = manualJobResolution; this.queue(this.defaults?.queue ?? constants.JOB_BUILDER_FALLBACK.queue); this.maxAttempts(this.defaults?.maxAttempts ?? constants.JOB_BUILDER_FALLBACK.maxAttempts); this.availableAt(this.defaults?.availableAt ?? constants.JOB_BUILDER_FALLBACK.availableAt); this.timeout(this.defaults?.timeout ?? constants.JOB_BUILDER_FALLBACK.timeout); this.unique(this.defaults?.uniqueness ?? constants.JOB_BUILDER_FALLBACK.uniqueness); this.with(...constants.JOB_BUILDER_FALLBACK.constructorArgs); this.retryDelay(this.defaults?.retryDelay ?? constants.JOB_BUILDER_FALLBACK.retryDelay); this.backoffStrategy(this.defaults?.backoffStrategy ?? constants.JOB_BUILDER_FALLBACK.backoffStrategy); } /** * Sets the constructor arguments for the job. * @param args The constructor arguments. * @returns This builder instance. */ with(...args) { this.constructorArgs = args; return this; } /** * Sets the queue name for the job. * @param queue The queue name. * @returns This builder instance. */ queue(queue) { this.queueName = queue; return this; } /** * Sets the timeout for the job in milliseconds. * @param ms Timeout in milliseconds. * @returns This builder instance. */ timeout(ms) { this.jobTimeout = ms; return this; } /** * Sets the uniqueness configuration for the job. * @param value Boolean or uniqueness config object. If true, uses an alive job uniqueness strategy (see {@link AliveJobUniqueness}). * If false, disables uniqueness. If an object, uses the custom uniqueness strategy. * @param value.withArgs If true, uniqueness is based on job class and job arguments. * If false, uniqueness is based only on the job class. * @param value.period If a period is provided, uses a fixed window uniqueness strategy (see {@link FixedWindowUniqueness}). * @returns This builder instance. * @see {@link UniquenessInput} for more details. */ unique(value) { if (typeof value === "boolean") { if (value) { const config = { type: "alive-job", withArgs: false, }; this.uniquenessConfig = config; } else { this.uniquenessConfig = undefined; // no uniqueness } } else { if (value.period) { this.uniquenessConfig = { type: "fixed-window", period: value.period, withArgs: value.withArgs, }; } else { this.uniquenessConfig = { type: "alive-job", withArgs: value.withArgs }; } } return this; } /** * Sets the maximum number of attempts for the job. * @param value The max attempts. * @returns This builder instance. */ maxAttempts(value) { this.jobMaxAttempts = value; return this; } /** * Sets the time when the job becomes available. * @param value The available date. * @returns This builder instance. */ availableAt(value) { this.jobAvailableAt = value; return this; } /** * Delay before retrying a failed job, in milliseconds. * * If "backoff_strategy" is "exponential", this value is used as the base delay for calculating exponential backoff. * * @param value The retry delay in milliseconds. * @returns This builder instance. */ retryDelay(value) { this.jobRetryDelay = value; return this; } /** * Strategy used for calculating backoff delays between retries. * - "exponential": Delays increase exponentially with each attempt. * - "fixed": Delays remain constant for each attempt. * * @param value The backoff strategy. * @returns This builder instance. */ backoffStrategy(value) { this.jobBackoffStrategy = value; return this; } async build(...args) { const job = new this.JobClass(...this.constructorArgs); if (!this.manualJobResolution) { // This resolves the job script path using exception handling. await job.ready(); } else { // If manual resolution is enabled, we skip automatic script resolution. // The user is responsible for ensuring the job class is properly exported // in the `sidequest.jobs.js` file. Object.assign(job, { script: manualLoader.MANUAL_SCRIPT_TAG }); } if (!job.script) { throw new Error(`Error on starting job ${job.className} could not detect source file.`); } const jobData = { queue: this.queueName, script: job.script, class: job.className, state: "waiting", args, constructor_args: this.constructorArgs, attempt: 0, max_attempts: this.jobMaxAttempts, available_at: this.jobAvailableAt, timeout: this.jobTimeout, uniqueness_config: this.uniquenessConfig, retry_delay: this.jobRetryDelay, backoff_strategy: this.jobBackoffStrategy, }; if (this.uniquenessConfig) { const uniqueness = core.UniquenessFactory.create(this.uniquenessConfig); jobData.unique_digest = uniqueness.digest(jobData); core.logger("JobBuilder").debug(`Job ${jobData.class} uniqueness digest: ${jobData.unique_digest}`); } return jobData; } /** * Enqueues the job with the specified arguments. * @param args Arguments to pass to the job's run method. * @returns A promise resolving to the created job data. */ async enqueue(...args) { const jobData = await this.build(...args); core.logger("JobBuilder").debug(`Enqueuing job ${jobData.class} with args: ${node_util.inspect(args)} and constructor args: ${node_util.inspect(this.constructorArgs)}`); return this.backend.createNewJob(jobData); } /** * Registers a recurring schedule to enqueue the job automatically based on a cron expression. * * This sets up an in-memory schedule that enqueues the job with the provided arguments * every time the cron expression is triggered. * * @remarks * - The schedule is **not persisted** to any database. It will be lost if the process restarts and must be re-registered at startup. * - You must call this method during application initialization to ensure the job is scheduled correctly. * - Uses node-cron's `noOverlap: true` option to prevent concurrent executions. * - The scheduled task is registered with the CronRegistry for proper cleanup during shutdown. * * @param cronExpression - A valid cron expression (node-cron compatible) that defines when the job should be enqueued. * @param args - Arguments to be passed to the job's `run` method on each scheduled execution. * * @returns The underlying `ScheduledTask` instance created by node-cron. * * @throws {Error} If the cron expression is invalid. */ async schedule(cronExpression, ...args) { if (!nodeCron.validate(cronExpression)) { throw new Error(`Invalid cron expression ${cronExpression}`); } // Build the job data using the provided arguments, // this ensures the scheduled state is going to be respected in cases where the builder was reused. // Includes class name, queue, timeout, uniqueness, etc. const jobData = await this.build(...args); // Freeze the job data to prevent future modifications. // Ensures the same payload is used on every scheduled execution. Object.freeze(jobData); core.logger("JobBuilder").debug(`Scheduling job ${jobData.class} with cron: "${cronExpression}", args: ${node_util.inspect(args)}, ` + `constructor args: ${node_util.inspect(this.constructorArgs)}`); const scheduledTask = nodeCron.schedule(cronExpression, async () => { const newJobData = Object.assign({}, jobData); core.logger("JobBuilder").debug(`Cron triggered for job ${newJobData.class} at ${newJobData.available_at.toISOString()} with args: ${node_util.inspect(args)}`); return this.backend.createNewJob(jobData); }, { noOverlap: true }); // Register the scheduled task with the ScheduledJobRegistry for proper later cleanup cronRegistry.ScheduledJobRegistry.register(scheduledTask); return scheduledTask; } } exports.JobBuilder = JobBuilder; //# sourceMappingURL=job-builder.cjs.map