@sidequest/engine
Version:
@sidequest/engine is the core engine of SideQuest, a distributed background job processing system for Node.js and TypeScript.
196 lines (192 loc) • 7.71 kB
JavaScript
'use strict';
var core = require('@sidequest/core');
var nodeCron = require('node-cron');
var constants = require('./constants.cjs');
/**
* Builder for creating and enqueuing jobs with custom configuration.
* @template T The job class type.
*/
class JobBuilder {
backend;
JobClass;
defaults;
constructorArgs;
queueName;
jobTimeout;
uniquenessConfig;
jobMaxAttempts;
jobAvailableAt;
/**
* Creates a new JobBuilder for the given job class.
* @param JobClass The job class constructor.
*/
constructor(backend, JobClass, defaults) {
this.backend = backend;
this.JobClass = JobClass;
this.defaults = defaults;
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);
}
/**
* 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;
}
async build(...args) {
const job = new this.JobClass(...this.constructorArgs);
await job.ready();
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,
};
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: ${JSON.stringify(args)}
and constructor args: ${JSON.stringify(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.
*
* @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: ${JSON.stringify(args)}, ` +
`constructor args: ${JSON.stringify(this.constructorArgs)}`);
return 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: ${JSON.stringify(args)}`);
return this.backend.createNewJob(jobData);
}, { noOverlap: true });
}
}
exports.JobBuilder = JobBuilder;
//# sourceMappingURL=job-builder.cjs.map