@sidequest/core
Version:
@sidequest/core is the core package of SideQuest, a distributed background job queue for Node.js and TypeScript applications.
97 lines (93 loc) • 3.65 kB
JavaScript
;
var crypto = require('crypto');
var safeStableStringify = require('safe-stable-stringify');
var logger = require('../logger.cjs');
/**
* Implements uniqueness checking using a fixed time window approach.
*
* This class generates unique digest keys for jobs based on their class name,
* execution time (truncated to a configured period), and optionally their arguments.
* Jobs with identical digests within the same time window are considered duplicates.
*
* The time window is determined by truncating the job's `available_at` timestamp
* to the specified period granularity (second, minute, hour, day, week, or month).
*
* @example
* ```typescript
* const uniqueness = new FixedWindowUniqueness({
* period: 'hour',
* withArgs: true
* });
*
* const digest = uniqueness.digest(jobData);
* // Returns SHA256 hash of "JobClass::time=2023-01-01T15:00:00.000Z::args=[...]::ctor=[...]"
* ```
*
* In this example, jobs of the same class scheduled within the same hour with the same arguments
* will have the same digest and thus won't be duplicated.
*/
class FixedWindowUniqueness {
config;
/**
* Creates a new FixedWindowUniqueness instance.
* @param config The fixed window configuration.
*/
constructor(config) {
this.config = config;
}
/**
* Computes a digest for the job data based on the configured time window and arguments.
* @param jobData The job data to compute the digest for.
* @returns The digest string.
*/
digest(jobData) {
const timeString = this.truncateDateString(jobData.available_at ?? new Date());
logger.logger("Core").debug(`Creating digest for job ${jobData.id} with time window`);
let key = `${jobData.class}::time=${timeString}`;
if (this.config.withArgs) {
key += "::args=" + safeStableStringify.stringify(jobData.args ?? []);
key += "::ctor=" + safeStableStringify.stringify(jobData.constructor_args ?? []);
}
logger.logger("Core").debug(`Uniqueness digest key: ${key}`);
return crypto.createHash("sha256").update(key).digest("hex");
}
/**
* Truncates a date to the configured time period granularity.
* @param date The date to truncate.
* @returns The truncated date as an ISO string.
*/
truncateDateString(date) {
const truncateDate = new Date(date);
switch (this.config.period) {
case "second":
truncateDate.setUTCMilliseconds(0);
break;
case "minute":
truncateDate.setUTCSeconds(0, 0);
break;
case "hour":
truncateDate.setUTCMinutes(0, 0, 0);
break;
case "day":
truncateDate.setUTCHours(0, 0, 0, 0);
break;
case "week": {
const day = truncateDate.getUTCDay(); // 0 = Sunday
const diff = truncateDate.getUTCDate() - day;
truncateDate.setUTCDate(diff);
truncateDate.setUTCHours(0, 0, 0, 0);
break;
}
case "month":
truncateDate.setUTCDate(1);
truncateDate.setUTCHours(0, 0, 0, 0);
break;
default:
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
throw new Error(`Unsupported period: ${this.config.period}`);
}
return truncateDate.toISOString();
}
}
exports.FixedWindowUniqueness = FixedWindowUniqueness;
//# sourceMappingURL=fixed-window-uniqueness.cjs.map