@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
JavaScript
'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