Review funkt

This commit is contained in:
2026-08-09 21:56:00 +00:00
parent f522ae9953
commit fe9dfa4519
11366 changed files with 2335077 additions and 97 deletions

View File

@@ -0,0 +1,17 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.AbortController = void 0;
// Note: this Polyfill is only needed for Node versions < 15.4.0
const node_abort_controller_1 = require("node-abort-controller");
let AbortControllerImpl;
// prefer native AbortController implementation if found
if (globalThis.AbortController) {
AbortControllerImpl =
globalThis.AbortController;
}
else {
AbortControllerImpl = node_abort_controller_1.AbortController;
}
class AbortController extends AbortControllerImpl {
}
exports.AbortController = AbortController;

View File

@@ -0,0 +1,171 @@
"use strict";
/**
* (c) 2017-2025 BullForce Labs AB, MIT Licensed.
* @see LICENSE.md
*
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.AsyncFifoQueue = void 0;
class Node {
constructor(value) {
this.value = undefined;
this.next = null;
this.value = value;
}
}
class LinkedList {
constructor() {
this.length = 0;
this.head = null;
this.tail = null;
}
push(value) {
const newNode = new Node(value);
if (!this.length) {
this.head = newNode;
}
else {
this.tail.next = newNode;
}
this.tail = newNode;
this.length += 1;
return newNode;
}
shift() {
if (!this.length) {
return null;
}
else {
const head = this.head;
this.head = this.head.next;
this.length -= 1;
return head;
}
}
}
/**
* AsyncFifoQueue
*
* A minimal FIFO queue for asynchronous operations. Allows adding asynchronous operations
* and consume them in the order they are resolved.
*/
class AsyncFifoQueue {
constructor(ignoreErrors = false) {
this.ignoreErrors = ignoreErrors;
/**
* A queue of completed promises. As the pending
* promises are resolved, they are added to this queue.
*/
this.queue = new LinkedList();
/**
* A set of pending promises.
*/
this.pending = new Set();
this.newPromise();
}
/**
* Adds a promise to the queue. When it resolves, its value is enqueued
* and, if a consumer is waiting via {@link fetch}, the next pending
* promise is resolved with the value.
*
* @param promise - The asynchronous operation to add to the queue.
*/
add(promise) {
this.pending.add(promise);
promise
.then(data => {
this.pending.delete(promise);
if (this.queue.length === 0) {
this.resolvePromise(data);
}
this.queue.push(data);
})
.catch(err => {
// Ignore errors
if (this.ignoreErrors) {
this.queue.push(undefined);
}
this.pending.delete(promise);
this.rejectPromise(err);
});
}
/**
* Waits for all currently pending promises to settle.
*
* @returns A promise that resolves once every in-flight promise has resolved.
*/
async waitAll() {
await Promise.all(this.pending);
}
/**
* Returns the total number of items currently tracked by the queue,
* including both pending (in-flight) promises and resolved items that
* have not yet been fetched.
*
* @returns The sum of pending and queued items.
*/
numTotal() {
return this.pending.size + this.queue.length;
}
/**
* Returns the number of promises that are still pending (in-flight).
*
* @returns The number of unresolved promises currently in the queue.
*/
numPending() {
return this.pending.size;
}
/**
* Returns the number of items that have already resolved but have not
* yet been fetched by a consumer.
*
* @returns The number of resolved items waiting to be consumed.
*/
numQueued() {
return this.queue.length;
}
resolvePromise(data) {
this.resolve(data);
this.newPromise();
}
rejectPromise(err) {
this.reject(err);
this.newPromise();
}
newPromise() {
this.nextPromise = new Promise((resolve, reject) => {
this.resolve = resolve;
this.reject = reject;
});
}
async wait() {
return this.nextPromise;
}
/**
* Fetches the next resolved item from the queue in FIFO order. If no
* items are currently resolved but pending promises exist, it waits
* until the next one resolves.
*
* @returns The next resolved value, or `undefined` if there are no
* pending or queued items.
*/
async fetch() {
var _a;
if (this.pending.size === 0 && this.queue.length === 0) {
return;
}
while (this.queue.length === 0) {
try {
await this.wait();
}
catch (err) {
// Ignore errors
if (!this.ignoreErrors) {
console.error('Unexpected Error in AsyncFifoQueue', err);
}
}
}
return (_a = this.queue.shift()) === null || _a === void 0 ? void 0 : _a.value;
}
}
exports.AsyncFifoQueue = AsyncFifoQueue;

60
node_modules/bullmq/dist/cjs/classes/backoffs.js generated vendored Normal file
View File

@@ -0,0 +1,60 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Backoffs = void 0;
class Backoffs {
static normalize(backoff) {
if (Number.isFinite(backoff)) {
return {
type: 'fixed',
delay: backoff,
};
}
else if (backoff) {
return backoff;
}
}
static calculate(backoff, attemptsMade, err, job, customStrategy) {
if (backoff) {
const strategy = lookupStrategy(backoff, customStrategy);
return strategy(attemptsMade, backoff.type, err, job);
}
}
}
exports.Backoffs = Backoffs;
Backoffs.builtinStrategies = {
fixed: function (delay, jitter = 0) {
return function () {
if (jitter > 0) {
const minDelay = delay * (1 - jitter);
return Math.floor(Math.random() * delay * jitter + minDelay);
}
else {
return delay;
}
};
},
exponential: function (delay, jitter = 0) {
return function (attemptsMade) {
if (jitter > 0) {
const maxDelay = Math.round(Math.pow(2, attemptsMade - 1) * delay);
const minDelay = maxDelay * (1 - jitter);
return Math.floor(Math.random() * maxDelay * jitter + minDelay);
}
else {
return Math.round(Math.pow(2, attemptsMade - 1) * delay);
}
};
},
};
function lookupStrategy(backoff, customStrategy) {
if (backoff.type in Backoffs.builtinStrategies) {
return Backoffs.builtinStrategies[backoff.type](backoff.delay, backoff.jitter);
}
else if (customStrategy) {
return customStrategy;
}
else {
throw new Error(`Unknown backoff strategy ${backoff.type}.
If a custom backoff strategy is used, specify it when the queue is created.`);
}
}

1032
node_modules/bullmq/dist/cjs/classes/bun-redis-client.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

98
node_modules/bullmq/dist/cjs/classes/child-pool.js generated vendored Normal file
View File

@@ -0,0 +1,98 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ChildPool = void 0;
const path = require("path");
const child_1 = require("./child");
const CHILD_KILL_TIMEOUT = 30000;
const supportCJS = () => {
return (typeof require === 'function' &&
typeof module === 'object' &&
typeof module.exports === 'object');
};
class ChildPool {
constructor({ mainFile = supportCJS()
? path.join(process.cwd(), 'dist/cjs/classes/main.js')
: path.join(process.cwd(), 'dist/esm/classes/main.js'), useWorkerThreads, workerForkOptions, workerThreadsOptions, }) {
this.retained = {};
this.free = {};
this.opts = {
mainFile,
useWorkerThreads,
workerForkOptions,
workerThreadsOptions,
};
}
async retain(processFile) {
let child = this.getFree(processFile).pop();
if (child) {
this.retained[child.pid] = child;
return child;
}
child = new child_1.Child(this.opts.mainFile, processFile, {
useWorkerThreads: this.opts.useWorkerThreads,
workerForkOptions: this.opts.workerForkOptions,
workerThreadsOptions: this.opts.workerThreadsOptions,
});
child.on('exit', this.remove.bind(this, child));
try {
await child.init();
// Check status here as well, in case the child exited before we could
// retain it.
if (child.exitCode !== null || child.signalCode !== null) {
throw new Error('Child exited before it could be retained');
}
this.retained[child.pid] = child;
return child;
}
catch (err) {
console.error(err);
// A child that failed to initialize (or exited during init) must never
// be released back into the free pool, otherwise it becomes a "zombie"
// that is reused for every subsequent job and fails them instantly.
// Kill and remove it so a fresh child is forked on the next retain.
// The child also exits itself after a failed init (see ChildProcessor),
// so this is normally a no-op; log any kill failure instead of silently
// swallowing it so a lingering child would not go unnoticed.
if (child.childProcess || child.worker) {
try {
this.kill(child, 'SIGKILL').catch(killErr => {
console.error('Failed to kill child after init error:', killErr);
});
}
catch (killErr) {
console.error('Failed to kill child after init error:', killErr);
}
}
throw err;
}
}
release(child) {
delete this.retained[child.pid];
this.getFree(child.processFile).push(child);
}
remove(child) {
delete this.retained[child.pid];
const free = this.getFree(child.processFile);
const childIndex = free.indexOf(child);
if (childIndex > -1) {
free.splice(childIndex, 1);
}
}
async kill(child, signal = 'SIGKILL') {
this.remove(child);
return child.kill(signal, CHILD_KILL_TIMEOUT);
}
async clean() {
const children = Object.values(this.retained).concat(this.getAllFree());
this.retained = {};
this.free = {};
await Promise.all(children.map(c => this.kill(c, 'SIGTERM')));
}
getFree(id) {
return (this.free[id] = this.free[id] || []);
}
getAllFree() {
return Object.values(this.free).reduce((first, second) => first.concat(second), []);
}
}
exports.ChildPool = ChildPool;

268
node_modules/bullmq/dist/cjs/classes/child-processor.js generated vendored Normal file
View File

@@ -0,0 +1,268 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ChildProcessor = void 0;
const abort_controller_1 = require("./abort-controller");
const enums_1 = require("../enums");
const utils_1 = require("../utils");
var ChildStatus;
(function (ChildStatus) {
ChildStatus[ChildStatus["Idle"] = 0] = "Idle";
ChildStatus[ChildStatus["Started"] = 1] = "Started";
ChildStatus[ChildStatus["Terminating"] = 2] = "Terminating";
ChildStatus[ChildStatus["Errored"] = 3] = "Errored";
})(ChildStatus || (ChildStatus = {}));
const RESPONSE_TIMEOUT = process.env.NODE_ENV === 'test' ? 500 : 5000;
/**
* ChildProcessor
*
* This class acts as the interface between a child process and it parent process
* so that jobs can be processed in different processes.
*
*/
class ChildProcessor {
constructor(send, receiver) {
this.send = send;
this.receiver = receiver;
}
async init(processorFile) {
var _a;
let processor;
try {
const { default: processorFn } = await import(processorFile);
processor = processorFn;
if (processor.default) {
// support es2015 module.
processor = processor.default;
}
if (typeof processor !== 'function') {
throw new Error('No function is exported in processor file');
}
}
catch (err) {
this.status = ChildStatus.Errored;
try {
await this.send({
cmd: enums_1.ParentCommand.InitFailed,
err: (0, utils_1.errorToJSON)(err),
});
}
finally {
// A child that failed to initialize cannot recover, and because the open
// IPC channel keeps its event loop alive it would never exit on its own.
// Exit explicitly (after attempting to send InitFailed) so the parent
// can never reuse a half-initialized "zombie" child. This is a
// belt-and-braces measure: ChildPool also kills the child, but exiting
// here guarantees termination even if the parent-side kill were to fail.
// In a worker thread this stops only the current worker, not the process.
process.exit((_a = process.exitCode) !== null && _a !== void 0 ? _a : 1);
}
}
const origProcessor = processor;
processor = function (job, token, signal) {
try {
return Promise.resolve(origProcessor(job, token, signal));
}
catch (err) {
return Promise.reject(err);
}
};
this.processor = processor;
this.status = ChildStatus.Idle;
await this.send({
cmd: enums_1.ParentCommand.InitCompleted,
});
}
async start(jobJson, token) {
if (this.status !== ChildStatus.Idle) {
return this.send({
cmd: enums_1.ParentCommand.Error,
err: (0, utils_1.errorToJSON)(new Error('cannot start a not idling child process')),
});
}
this.status = ChildStatus.Started;
this.abortController = new abort_controller_1.AbortController();
this.currentJobPromise = (async () => {
try {
const job = this.wrapJob(jobJson, this.send);
const result = await this.processor(job, token, this.abortController.signal);
await this.send({
cmd: enums_1.ParentCommand.Completed,
value: typeof result === 'undefined' ? null : result,
});
}
catch (err) {
await this.send({
cmd: enums_1.ParentCommand.Failed,
value: (0, utils_1.errorToJSON)(!err.message ? new Error(err) : err),
});
}
finally {
this.status = ChildStatus.Idle;
this.currentJobPromise = undefined;
this.abortController = undefined;
}
})();
}
/**
* Cancels the currently running job by aborting its signal.
* @param reason - Optional reason for the cancellation
*/
cancel(reason) {
if (this.abortController) {
this.abortController.abort(reason);
}
}
async stop() { }
async waitForCurrentJobAndExit() {
this.status = ChildStatus.Terminating;
try {
await this.currentJobPromise;
}
finally {
process.exit(process.exitCode || 0);
}
}
/**
* Enhance the given job argument with some functions
* that can be called from the sandboxed job processor.
*
* Note, the `job` argument is a JSON deserialized message
* from the main node process to this forked child process,
* the functions on the original job object are not in tact.
* The wrapped job adds back some of those original functions.
*/
wrapJob(job, send) {
const wrappedJob = Object.assign(Object.assign({}, job), { queueQualifiedName: job.queueQualifiedName, data: JSON.parse(job.data || '{}'), opts: job.opts, returnValue: JSON.parse(job.returnvalue || '{}'),
/*
* Proxy `updateProgress` function, should works as `progress` function.
*/
async updateProgress(progress) {
// Locally store reference to new progress value
// so that we can return it from this process synchronously.
this.progress = progress;
// Send message to update job progress.
await send({
cmd: enums_1.ParentCommand.Progress,
value: progress,
});
},
/*
* Proxy job `log` function.
*/
log: async (row) => {
await send({
cmd: enums_1.ParentCommand.Log,
value: row,
});
},
/*
* Proxy `moveToDelayed` function.
*/
moveToDelayed: async (timestamp, token) => {
await send({
cmd: enums_1.ParentCommand.MoveToDelayed,
value: { timestamp, token },
});
},
/*
* Proxy `moveToWait` function.
*/
moveToWait: async (token) => {
await send({
cmd: enums_1.ParentCommand.MoveToWait,
value: { token },
});
},
/*
* Proxy `moveToWaitingChildren` function.
*/
moveToWaitingChildren: async (token, opts) => {
const requestId = Math.random().toString(36).substring(2, 15);
await send({
requestId,
cmd: enums_1.ParentCommand.MoveToWaitingChildren,
value: { token, opts },
});
return waitResponse(requestId, this.receiver, RESPONSE_TIMEOUT, 'moveToWaitingChildren');
},
/*
* Proxy `updateData` function.
*/
updateData: async (data) => {
await send({
cmd: enums_1.ParentCommand.Update,
value: data,
});
wrappedJob.data = data;
},
/**
* Proxy `getChildrenValues` function.
*/
getChildrenValues: async () => {
const requestId = Math.random().toString(36).substring(2, 15);
await send({
requestId,
cmd: enums_1.ParentCommand.GetChildrenValues,
});
return waitResponse(requestId, this.receiver, RESPONSE_TIMEOUT, 'getChildrenValues');
},
/**
* Proxy `getIgnoredChildrenFailures` function.
*
* This method sends a request to retrieve the failures of ignored children
* and waits for a response from the parent process.
*
* @returns - A promise that resolves with the ignored children failures.
* The exact structure of the returned data depends on the parent process implementation.
*/
getIgnoredChildrenFailures: async () => {
const requestId = Math.random().toString(36).substring(2, 15);
await send({
requestId,
cmd: enums_1.ParentCommand.GetIgnoredChildrenFailures,
});
return waitResponse(requestId, this.receiver, RESPONSE_TIMEOUT, 'getIgnoredChildrenFailures');
},
/**
* Proxy `getDependenciesCount` function.
*/
getDependenciesCount: async (opts) => {
const requestId = Math.random().toString(36).substring(2, 15);
await send({
requestId,
cmd: enums_1.ParentCommand.GetDependenciesCount,
value: opts,
});
return waitResponse(requestId, this.receiver, RESPONSE_TIMEOUT, 'getDependenciesCount');
},
/**
* Proxy `getDependencies` function.
*/
getDependencies: async (opts) => {
const requestId = Math.random().toString(36).substring(2, 15);
await send({
requestId,
cmd: enums_1.ParentCommand.GetDependencies,
value: opts,
});
return waitResponse(requestId, this.receiver, RESPONSE_TIMEOUT, 'getDependencies');
} });
return wrappedJob;
}
}
exports.ChildProcessor = ChildProcessor;
const waitResponse = async (requestId, receiver, timeout, cmd) => {
return new Promise((resolve, reject) => {
const listener = (msg) => {
if (msg.requestId === requestId) {
resolve(msg.value);
receiver.off('message', listener);
}
};
receiver.on('message', listener);
setTimeout(() => {
receiver.off('message', listener);
reject(new Error(`TimeoutError: ${cmd} timed out in (${timeout}ms)`));
}, timeout);
});
};

220
node_modules/bullmq/dist/cjs/classes/child.js generated vendored Normal file
View File

@@ -0,0 +1,220 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Child = void 0;
const child_process_1 = require("child_process");
const net_1 = require("net");
const worker_threads_1 = require("worker_threads");
const enums_1 = require("../enums");
const events_1 = require("events");
/**
* @see https://nodejs.org/api/process.html#process_exit_codes
*/
const exitCodesErrors = {
1: 'Uncaught Fatal Exception',
2: 'Unused',
3: 'Internal JavaScript Parse Error',
4: 'Internal JavaScript Evaluation Failure',
5: 'Fatal Error',
6: 'Non-function Internal Exception Handler',
7: 'Internal Exception Handler Run-Time Failure',
8: 'Unused',
9: 'Invalid Argument',
10: 'Internal JavaScript Run-Time Failure',
12: 'Invalid Debug Argument',
13: 'Unfinished Top-Level Await',
};
/**
* Child class
*
* This class is used to create a child process or worker thread, and allows using
* isolated processes or threads for processing jobs.
*
*/
class Child extends events_1.EventEmitter {
constructor(mainFile, processFile, opts = {
useWorkerThreads: false,
}) {
super();
this.mainFile = mainFile;
this.processFile = processFile;
this.opts = opts;
this._exitCode = null;
this._signalCode = null;
this._killed = false;
}
get pid() {
if (this.childProcess) {
return this.childProcess.pid;
}
else if (this.worker) {
// Worker threads pids can become negative when they are terminated
// so we need to use the absolute value to index the retained object
return Math.abs(this.worker.threadId);
}
else {
throw new Error('No child process or worker thread');
}
}
get exitCode() {
return this._exitCode;
}
get signalCode() {
return this._signalCode;
}
get killed() {
if (this.childProcess) {
return this.childProcess.killed;
}
return this._killed;
}
async init() {
var _a, _b;
const execArgv = await convertExecArgv(process.execArgv);
let parent;
if (this.opts.useWorkerThreads) {
this.worker = parent = new worker_threads_1.Worker(this.mainFile, Object.assign({ execArgv, stdin: true, stdout: true, stderr: true }, (this.opts.workerThreadsOptions
? this.opts.workerThreadsOptions
: {})));
}
else {
this.childProcess = parent = (0, child_process_1.fork)(this.mainFile, [], Object.assign({ execArgv, stdio: 'pipe' }, (this.opts.workerForkOptions ? this.opts.workerForkOptions : {})));
}
parent.on('exit', (exitCode, signalCode) => {
this._exitCode = exitCode;
// Coerce to null if undefined for backwards compatibility
signalCode = typeof signalCode === 'undefined' ? null : signalCode;
this._signalCode = signalCode;
this._killed = true;
this.emit('exit', exitCode, signalCode);
// Clean all listeners, we do not expect any more events after "exit"
parent.removeAllListeners();
this.removeAllListeners();
});
parent.on('error', (...args) => this.emit('error', ...args));
parent.on('message', (...args) => this.emit('message', ...args));
parent.on('close', (...args) => this.emit('close', ...args));
// `parent.stdout`/`parent.stderr` may be null when the underlying runtime
// does not pipe child stdio (e.g. Bun ignores `worker_threads` stdout/stderr
// options, and Node returns null when `stdio: 'ignore'` is passed in
// `workerForkOptions`). Guard the pipe calls so initialization does not throw.
// See https://github.com/taskforcesh/bullmq/issues/2232
(_a = parent.stdout) === null || _a === void 0 ? void 0 : _a.pipe(process.stdout);
(_b = parent.stderr) === null || _b === void 0 ? void 0 : _b.pipe(process.stderr);
await this.initChild();
}
async send(msg) {
return new Promise((resolve, reject) => {
if (this.childProcess) {
this.childProcess.send(msg, (err) => {
if (err) {
reject(err);
}
else {
resolve();
}
});
}
else if (this.worker) {
resolve(this.worker.postMessage(msg));
}
else {
resolve();
}
});
}
killProcess(signal = 'SIGKILL') {
if (this.childProcess) {
this.childProcess.kill(signal);
}
else if (this.worker) {
this.worker.terminate();
}
}
async kill(signal = 'SIGKILL', timeoutMs) {
if (this.hasProcessExited()) {
return;
}
const onExit = onExitOnce(this.childProcess || this.worker);
this.killProcess(signal);
if (timeoutMs !== undefined && (timeoutMs === 0 || isFinite(timeoutMs))) {
const timeoutHandle = setTimeout(() => {
if (!this.hasProcessExited()) {
this.killProcess('SIGKILL');
}
}, timeoutMs);
await onExit;
clearTimeout(timeoutHandle);
}
await onExit;
}
async initChild() {
const onComplete = new Promise((resolve, reject) => {
const onMessageHandler = (msg) => {
if (!Object.values(enums_1.ParentCommand).includes(msg.cmd)) {
return;
}
if (msg.cmd === enums_1.ParentCommand.InitCompleted) {
resolve();
}
else if (msg.cmd === enums_1.ParentCommand.InitFailed) {
const err = new Error();
err.stack = msg.err.stack;
err.message = msg.err.message;
reject(err);
}
this.off('message', onMessageHandler);
this.off('close', onCloseHandler);
};
const onCloseHandler = (code, signal) => {
if (code > 128) {
code -= 128;
}
const msg = exitCodesErrors[code] || `Unknown exit code ${code}`;
reject(new Error(`Error initializing child: ${msg} and signal ${signal}`));
this.off('message', onMessageHandler);
this.off('close', onCloseHandler);
};
this.on('message', onMessageHandler);
this.on('close', onCloseHandler);
});
await this.send({
cmd: enums_1.ChildCommand.Init,
value: this.processFile,
});
await onComplete;
}
hasProcessExited() {
return !!(this.exitCode !== null || this.signalCode);
}
}
exports.Child = Child;
function onExitOnce(child) {
return new Promise(resolve => {
child.once('exit', () => resolve());
});
}
const getFreePort = async () => {
return new Promise(resolve => {
const server = (0, net_1.createServer)();
server.listen(0, () => {
const { port } = server.address();
server.close(() => resolve(port));
});
});
};
const convertExecArgv = async (execArgv) => {
const standard = [];
const convertedArgs = [];
for (let i = 0; i < execArgv.length; i++) {
const arg = execArgv[i];
if (arg.indexOf('--inspect') === -1) {
standard.push(arg);
}
else {
const argName = arg.split('=')[0];
const port = await getFreePort();
convertedArgs.push(`${argName}=${port}`);
}
}
return standard.concat(convertedArgs);
};

View File

@@ -0,0 +1,28 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ConnectionClosedError = exports.CONNECTION_CLOSED_ERROR_MSG = void 0;
/**
* Error message used when a connection is closed.
*
* This mirrors the constant exported by ioredis (`ioredis/built/utils`) but is
* defined locally so that BullMQ can be used without ioredis installed (for
* example, when using the PostgreSQL backend).
*/
exports.CONNECTION_CLOSED_ERROR_MSG = 'Connection is closed.';
/**
* Thrown by any BullMQ Redis adapter (ioredis, node-redis, Bun, …) when a
* command fails because the connection is already closed or was closed
* mid-flight.
*
* Using a single well-known class lets {@link isNotConnectionError} do a
* structural `instanceof` check rather than fragile message-substring matching.
*/
class ConnectionClosedError extends Error {
constructor(message, cause) {
super(message !== null && message !== void 0 ? message : exports.CONNECTION_CLOSED_ERROR_MSG);
this.cause = cause;
this.name = 'ConnectionClosedError';
Object.setPrototypeOf(this, new.target.prototype);
}
}
exports.ConnectionClosedError = ConnectionClosedError;

View File

@@ -0,0 +1,19 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.DelayedError = exports.DELAYED_ERROR = void 0;
exports.DELAYED_ERROR = 'bullmq:movedToDelayed';
/**
* DelayedError
*
* Error to be thrown when job is moved to delayed state
* from job in active state.
*
*/
class DelayedError extends Error {
constructor(message = exports.DELAYED_ERROR) {
super(message);
this.name = this.constructor.name;
Object.setPrototypeOf(this, new.target.prototype);
}
}
exports.DelayedError = DelayedError;

9
node_modules/bullmq/dist/cjs/classes/errors/index.js generated vendored Normal file
View File

@@ -0,0 +1,9 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
tslib_1.__exportStar(require("./connection-closed-error"), exports);
tslib_1.__exportStar(require("./delayed-error"), exports);
tslib_1.__exportStar(require("./rate-limit-error"), exports);
tslib_1.__exportStar(require("./unrecoverable-error"), exports);
tslib_1.__exportStar(require("./waiting-children-error"), exports);
tslib_1.__exportStar(require("./waiting-error"), exports);

View File

@@ -0,0 +1,18 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RateLimitError = exports.RATE_LIMIT_ERROR = void 0;
exports.RATE_LIMIT_ERROR = 'bullmq:rateLimitExceeded';
/**
* RateLimitError
*
* Error to be thrown when queue reaches a rate limit.
*
*/
class RateLimitError extends Error {
constructor(message = exports.RATE_LIMIT_ERROR) {
super(message);
this.name = this.constructor.name;
Object.setPrototypeOf(this, new.target.prototype);
}
}
exports.RateLimitError = RateLimitError;

View File

@@ -0,0 +1,19 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.UnrecoverableError = exports.UNRECOVERABLE_ERROR = void 0;
exports.UNRECOVERABLE_ERROR = 'bullmq:unrecoverable';
/**
* UnrecoverableError
*
* Error to move a job to failed even if the attemptsMade
* are lower than the expected limit.
*
*/
class UnrecoverableError extends Error {
constructor(message = exports.UNRECOVERABLE_ERROR) {
super(message);
this.name = this.constructor.name;
Object.setPrototypeOf(this, new.target.prototype);
}
}
exports.UnrecoverableError = UnrecoverableError;

View File

@@ -0,0 +1,19 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.WaitingChildrenError = exports.WAITING_CHILDREN_ERROR = void 0;
exports.WAITING_CHILDREN_ERROR = 'bullmq:movedToWaitingChildren';
/**
* WaitingChildrenError
*
* Error to be thrown when job is moved to waiting-children state
* from job in active state.
*
*/
class WaitingChildrenError extends Error {
constructor(message = exports.WAITING_CHILDREN_ERROR) {
super(message);
this.name = this.constructor.name;
Object.setPrototypeOf(this, new.target.prototype);
}
}
exports.WaitingChildrenError = WaitingChildrenError;

View File

@@ -0,0 +1,18 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.WaitingError = exports.WAITING_ERROR = void 0;
exports.WAITING_ERROR = 'bullmq:movedToWait';
/**
* WaitingError
*
* Error to be thrown when job is moved to wait or prioritized state
* from job in active state.
*/
class WaitingError extends Error {
constructor(message = exports.WAITING_ERROR) {
super(message);
this.name = this.constructor.name;
Object.setPrototypeOf(this, new.target.prototype);
}
}
exports.WaitingError = WaitingError;

View File

@@ -0,0 +1,55 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.finishedErrors = finishedErrors;
const enums_1 = require("../enums");
const unrecoverable_error_1 = require("./errors/unrecoverable-error");
/**
* Builds the canonical `Error` for a negative status code returned by a backend
* operation (Lua script, SQL function, …). Shared by every backend so that the
* error messages a caller sees are identical regardless of the datastore.
*
* The resulting error carries the numeric `code` so callers can branch on it.
*/
function finishedErrors({ code, jobId, parentKey, command, state, }) {
let error;
switch (code) {
case enums_1.ErrorCode.JobNotExist:
error = new Error(`Missing key for job ${jobId}. ${command}`);
break;
case enums_1.ErrorCode.JobLockNotExist:
error = new Error(`Missing lock for job ${jobId}. ${command}`);
break;
case enums_1.ErrorCode.JobNotInState:
error = new Error(`Job ${jobId} is not in the ${state} state. ${command}`);
break;
case enums_1.ErrorCode.JobPendingChildren:
error = new Error(`Job ${jobId} has pending dependencies. ${command}`);
break;
case enums_1.ErrorCode.ParentJobNotExist:
error = new Error(`Missing key for parent job ${parentKey}. ${command}`);
break;
case enums_1.ErrorCode.JobLockMismatch:
error = new Error(`Lock mismatch for job ${jobId}. Cmd ${command} from ${state}`);
break;
case enums_1.ErrorCode.ParentJobCannotBeReplaced:
error = new Error(`The parent job ${parentKey} cannot be replaced. ${command}`);
break;
case enums_1.ErrorCode.JobBelongsToJobScheduler:
error = new Error(`Job ${jobId} belongs to a job scheduler and cannot be removed directly. ${command}`);
break;
case enums_1.ErrorCode.JobHasFailedChildren:
error = new unrecoverable_error_1.UnrecoverableError(`Cannot complete job ${jobId} because it has at least one failed child. ${command}`);
break;
case enums_1.ErrorCode.SchedulerJobIdCollision:
error = new Error(`Cannot create job scheduler iteration - job ID already exists. ${command}`);
break;
case enums_1.ErrorCode.SchedulerJobSlotsBusy:
error = new Error(`Cannot create job scheduler iteration - current and next time slots already have jobs. ${command}`);
break;
default:
error = new Error(`Unknown code ${code} error for ${jobId}. ${command}`);
}
// Add the code property to the error object
error.code = code;
return error;
}

435
node_modules/bullmq/dist/cjs/classes/flow-producer.js generated vendored Normal file
View File

@@ -0,0 +1,435 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.FlowProducer = void 0;
const events_1 = require("events");
const utils_1 = require("../utils");
const create_backend_1 = require("../utils/create-backend");
const job_1 = require("./job");
const enums_1 = require("../enums");
/**
* This class allows to add jobs with dependencies between them in such
* a way that it is possible to build complex flows.
* Note: A flow is a tree-like structure of jobs that depend on each other.
* Whenever the children of a given parent are completed, the parent
* will be processed, being able to access the children's result data.
* All Jobs can be in different queues, either children or parents,
*/
class FlowProducer extends events_1.EventEmitter {
constructor(opts = { connection: {} }, backendFactory = (0, create_backend_1.getDefaultBackendFactory)()) {
super();
this.opts = opts;
this.opts = Object.assign({}, opts);
// The flow producer is not bound to a single queue: each flow entry carries
// its own queue identity, so the backend is created with an empty name.
this.backend = backendFactory('', this.opts);
this.backend.on('error', (error) => {
if (this.listenerCount('error') > 0) {
this.emit('error', error);
}
});
this.backend.on('close', () => {
if (!this.closing) {
this.emit('ioredis:close');
}
});
if (opts === null || opts === void 0 ? void 0 : opts.telemetry) {
this.telemetry = opts.telemetry;
}
}
emit(event, ...args) {
return super.emit(event, ...args);
}
off(eventName, listener) {
super.off(eventName, listener);
return this;
}
on(event, listener) {
super.on(event, listener);
return this;
}
once(event, listener) {
super.once(event, listener);
return this;
}
/**
* Helper to easily extend Job class calls.
*/
get Job() {
return job_1.Job;
}
waitUntilReady() {
return this.backend.waitUntilReady();
}
/**
* Returns the datastore backend that powers this flow producer.
*
* The backend owns its connection and exposes every datastore-agnostic
* operation through {@link IQueueBackend}. Datastore-specific escape hatches
* (e.g. the raw Redis client) live on the concrete backend implementation,
* and are exposed here when the flow producer is parameterized on that
* concrete backend type (the default is the Redis backend).
*/
getBackend() {
return this.backend;
}
/**
* Adds a flow.
*
* This call would be atomic, either it fails and no jobs will
* be added to the queues, or it succeeds and all jobs will be added.
*
* @param flow - an object with a tree-like structure where children jobs
* will be processed before their parents.
* @param opts - options that will be applied to the flow object.
*/
async add(flow, opts) {
if (this.closing) {
return;
}
this.validateFlowJobs([flow]);
// Ensure the backend (and thus the connection) is ready before building
// the per-node queue contexts used to create jobs.
await this.backend.waitUntilReady();
const flowOpts = flow === null || flow === void 0 ? void 0 : flow.opts;
const parentOpts = flowOpts && 'parent' in flowOpts ? flowOpts.parent : undefined;
const parentKey = (0, utils_1.getParentKey)(parentOpts);
const parentDependenciesKey = parentKey
? `${parentKey}:dependencies`
: undefined;
return (0, utils_1.trace)(this.telemetry, enums_1.SpanKind.PRODUCER, flow.queueName, 'addFlow', flow.queueName, async (span) => {
span === null || span === void 0 ? void 0 : span.setAttributes({
[enums_1.TelemetryAttributes.FlowName]: flow.name,
});
const entries = [];
const jobsTree = await this.addNode({
entries,
node: flow,
queuesOpts: opts === null || opts === void 0 ? void 0 : opts.queuesOptions,
parent: {
parentOpts,
parentDependenciesKey,
},
});
const results = await this.backend.addFlow(entries);
const [result] = results || [];
if (result) {
const [err, jobId] = result;
if (err) {
throw err;
}
if (typeof jobId === 'number' && jobId < 0) {
throw this.toFlowError(jobId, parentKey);
}
if (typeof jobId === 'string') {
jobsTree.job.id = jobId;
}
}
return jobsTree;
});
}
/**
* Get a flow.
*
* @param opts - an object with options for getting a JobNode.
*/
async getFlow(opts) {
if (this.closing) {
return;
}
await this.backend.waitUntilReady();
const updatedOpts = Object.assign({
depth: 10,
maxChildren: 20,
prefix: this.opts.prefix,
}, opts);
const jobsTree = this.getNode(updatedOpts);
return jobsTree;
}
/**
* Adds multiple flows.
*
* A flow is a tree-like structure of jobs that depend on each other.
* Whenever the children of a given parent are completed, the parent
* will be processed, being able to access the children's result data.
*
* All Jobs can be in different queues, either children or parents,
* however this call would be atomic, either it fails and no jobs will
* be added to the queues, or it succeeds and all jobs will be added.
*
* @param flows - an array of objects with a tree-like structure where children jobs
* will be processed before their parents.
*/
async addBulk(flows) {
if (this.closing) {
return;
}
this.validateFlowJobs(flows);
// Ensure the backend (and thus the connection) is ready before building
// the per-node queue contexts used to create jobs.
await this.backend.waitUntilReady();
return (0, utils_1.trace)(this.telemetry, enums_1.SpanKind.PRODUCER, '', 'addBulkFlows', '', async (span) => {
span === null || span === void 0 ? void 0 : span.setAttributes({
[enums_1.TelemetryAttributes.BulkCount]: flows.length,
[enums_1.TelemetryAttributes.BulkNames]: flows
.map(flow => flow.name)
.join(','),
});
const entries = [];
const jobsTrees = await this.addNodes(entries, flows);
const results = await this.backend.addFlow(entries);
for (let index = 0; index < jobsTrees.length; ++index) {
const result = results === null || results === void 0 ? void 0 : results[index];
if (!result) {
continue;
}
const [err, jobId] = result;
if (!err && typeof jobId === 'string') {
jobsTrees[index].job.id = jobId;
}
}
return jobsTrees;
});
}
/**
* Add a node (job) of a flow to the queue. This method will recursively
* add all its children as well. Note that a given job can potentially be
* a parent and a child job at the same time depending on where it is located
* in the tree hierarchy.
*
* @param multi - IRedisTransaction
* @param node - the node representing a job to be added to some queue
* @param parent - parent data sent to children to create the "links" to their parent
* @returns
*/
async addNode({ entries, node, parent, queuesOpts, }) {
var _a, _b;
const prefix = node.prefix || this.opts.prefix;
const queue = this.queueFromNode(node, prefix);
const queueOpts = queuesOpts && queuesOpts[node.queueName];
const jobsOpts = (_a = queueOpts === null || queueOpts === void 0 ? void 0 : queueOpts.defaultJobOptions) !== null && _a !== void 0 ? _a : {};
const jobId = ((_b = node.opts) === null || _b === void 0 ? void 0 : _b.jobId) || (0, utils_1.randomUUID)();
return (0, utils_1.trace)(this.telemetry, enums_1.SpanKind.PRODUCER, node.queueName, 'addNode', node.queueName, async (span, srcPropagationMetadata) => {
var _a, _b;
span === null || span === void 0 ? void 0 : span.setAttributes({
[enums_1.TelemetryAttributes.JobName]: node.name,
[enums_1.TelemetryAttributes.JobId]: jobId,
});
const opts = node.opts;
let telemetry = opts === null || opts === void 0 ? void 0 : opts.telemetry;
if (srcPropagationMetadata && opts) {
const omitContext = (_a = opts.telemetry) === null || _a === void 0 ? void 0 : _a.omitContext;
const telemetryMetadata = ((_b = opts.telemetry) === null || _b === void 0 ? void 0 : _b.metadata) ||
(!omitContext && srcPropagationMetadata);
if (telemetryMetadata || omitContext) {
telemetry = {
metadata: telemetryMetadata,
omitContext,
};
}
}
const job = new this.Job(queue, node.name, node.data, Object.assign(Object.assign(Object.assign({}, jobsOpts), opts), { parent: parent === null || parent === void 0 ? void 0 : parent.parentOpts, telemetry }), jobId);
const parentKey = (0, utils_1.getParentKey)(parent === null || parent === void 0 ? void 0 : parent.parentOpts);
if (node.children && node.children.length > 0) {
// Create the parent job, it will be a job in status "waiting-children".
const parentId = jobId;
await this.collectFlowEntry(entries, job, {
parentDependenciesKey: parent === null || parent === void 0 ? void 0 : parent.parentDependenciesKey,
addToWaitingChildren: true,
parentKey,
});
// Queue identity is owned by the backend (the `queue` object above is
// bound to this node's queue via the backend's `forQueue`).
const parentDependenciesKey = `${queue.toKey(parentId)}:dependencies`;
const children = await this.addChildren({
entries,
nodes: node.children,
parent: {
parentOpts: {
id: parentId,
queue: queue.qualifiedName,
},
parentDependenciesKey,
},
queuesOpts,
});
return { job, children };
}
else {
await this.collectFlowEntry(entries, job, {
parentDependenciesKey: parent === null || parent === void 0 ? void 0 : parent.parentDependenciesKey,
parentKey,
});
return { job };
}
});
}
/**
* Adds nodes (jobs) of multiple flows to the queue. This method will recursively
* add all its children as well. Note that a given job can potentially be
* a parent and a child job at the same time depending on where it is located
* in the tree hierarchy.
*
* @param multi - IRedisTransaction
* @param nodes - the nodes representing jobs to be added to some queue
* @returns
*/
/**
* Collects a single job insert for a flow, preserving the same await point
* as the previous transaction-based insert so that the relative order of
* entries (in particular, roots before their descendants) is unchanged.
*/
async collectFlowEntry(entries, job, parentOpts) {
entries.push(job.toFlowEntry(parentOpts));
}
addNodes(entries, nodes) {
return Promise.all(nodes.map(node => {
const nodeOpts = node === null || node === void 0 ? void 0 : node.opts;
const parentOpts = nodeOpts && 'parent' in nodeOpts ? nodeOpts.parent : undefined;
const parentKey = (0, utils_1.getParentKey)(parentOpts);
const parentDependenciesKey = parentKey
? `${parentKey}:dependencies`
: undefined;
return this.addNode({
entries,
node,
parent: {
parentOpts,
parentDependenciesKey,
},
});
}));
}
async getNode(node) {
const queue = this.queueFromNode(node, node.prefix);
const job = await this.Job.fromId(queue, node.id);
if (job) {
const { processed = {}, unprocessed = [], failed = [], ignored = {}, } = await job.getDependencies({
failed: {
count: node.maxChildren,
},
processed: {
count: node.maxChildren,
},
unprocessed: {
count: node.maxChildren,
},
ignored: {
count: node.maxChildren,
},
});
const processedKeys = Object.keys(processed);
const ignoredKeys = Object.keys(ignored);
const childrenCount = processedKeys.length +
unprocessed.length +
ignoredKeys.length +
failed.length;
const newDepth = node.depth - 1;
if (childrenCount > 0 && newDepth) {
const children = await this.getChildren([...processedKeys, ...unprocessed, ...failed, ...ignoredKeys], newDepth, node.maxChildren);
return { job, children };
}
else {
return { job };
}
}
}
validateFlowJobs(nodes) {
for (const node of nodes) {
const children = node.children;
if (children && children.length > 0) {
const nodeOpts = node.opts;
const hasDeduplication = nodeOpts && 'deduplication' in nodeOpts && nodeOpts.deduplication;
if (hasDeduplication) {
throw new Error('Deduplication options cannot be used on flow nodes with children');
}
this.validateFlowJobs(children);
}
}
}
addChildren({ entries, nodes, parent, queuesOpts }) {
return Promise.all(nodes.map(node => this.addNode({ entries, node, parent, queuesOpts })));
}
getChildren(childrenKeys, depth, maxChildren) {
const getChild = (key) => {
const { prefix, queueName, id } = this.backend.parseNodeKey(key);
return this.getNode({
id,
queueName,
prefix,
depth,
maxChildren,
});
};
return Promise.all([...childrenKeys.map(getChild)]);
}
/**
* Helper factory method that creates a queue-like object
* required to create jobs in any queue.
*
* @param node - The flow node containing the queue name and other job options.
* @param prefix - The key prefix for the queue (honored by the Redis backend only).
* @returns A queue-like object with the keys, identity and backend needed to create jobs.
*/
queueFromNode(node, prefix) {
// Queue identity and key building are owned by the backend (the Redis
// backend encodes the key `prefix`; other backends format their own
// identity). The flow's own backend is queue-agnostic, so we ask it for a
// sibling bound to this node's queue.
const backend = this.backend.forQueue(node.queueName, prefix);
return {
name: node.queueName,
keys: backend.keys,
toKey: (type) => backend.toKey(type),
opts: { prefix, connection: {} },
qualifiedName: backend.qualifiedName,
closing: this.closing,
backend,
waitUntilReady: async () => {
await this.backend.waitUntilReady();
},
removeListener: this.removeListener.bind(this),
emit: this.emit.bind(this),
on: this.on.bind(this),
trace: async () => { },
};
}
/**
* Translates numeric addJob Lua error codes returned by root flow exec.
*
* @param code - Numeric error code returned from Redis.
* @param parentKey - Parent key for contextual error messages.
*/
toFlowError(code, parentKey) {
let error;
switch (code) {
case enums_1.ErrorCode.ParentJobNotExist:
error = new Error(`Missing key for parent job ${parentKey}. addJob`);
break;
case enums_1.ErrorCode.ParentJobCannotBeReplaced:
error = new Error(`The parent job ${parentKey} cannot be replaced. addJob`);
break;
default:
error = new Error(`Unknown code ${code} error for addJob`);
}
error.code = code;
return error;
}
/**
*
* Closes the connection and returns a promise that resolves when the connection is closed.
*/
async close() {
if (!this.closing) {
this.closing = this.backend.close();
}
await this.closing;
}
/**
*
* Force disconnects a connection.
*/
disconnect() {
return this.backend.disconnect();
}
}
exports.FlowProducer = FlowProducer;

29
node_modules/bullmq/dist/cjs/classes/index.js generated vendored Normal file
View File

@@ -0,0 +1,29 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
tslib_1.__exportStar(require("./async-fifo-queue"), exports);
tslib_1.__exportStar(require("./backoffs"), exports);
tslib_1.__exportStar(require("./child"), exports);
tslib_1.__exportStar(require("./child-pool"), exports);
tslib_1.__exportStar(require("./child-processor"), exports);
tslib_1.__exportStar(require("./errors"), exports);
tslib_1.__exportStar(require("./flow-producer"), exports);
tslib_1.__exportStar(require("./ioredis-client"), exports);
tslib_1.__exportStar(require("./node-redis-client"), exports);
tslib_1.__exportStar(require("./bun-redis-client"), exports);
tslib_1.__exportStar(require("./valkey-glide-client"), exports);
tslib_1.__exportStar(require("./job"), exports);
tslib_1.__exportStar(require("./job-scheduler"), exports);
// export * from './main'; this file must not be exported
// export * from './main-worker'; this file must not be exported
tslib_1.__exportStar(require("./lock-manager"), exports);
tslib_1.__exportStar(require("./queue-base"), exports);
tslib_1.__exportStar(require("./queue-events"), exports);
tslib_1.__exportStar(require("./queue-events-producer"), exports);
tslib_1.__exportStar(require("./queue-getters"), exports);
tslib_1.__exportStar(require("./queue-keys"), exports);
tslib_1.__exportStar(require("./queue"), exports);
tslib_1.__exportStar(require("./redis-connection"), exports);
tslib_1.__exportStar(require("./sandbox"), exports);
tslib_1.__exportStar(require("./redis-queue-backend"), exports);
tslib_1.__exportStar(require("./worker"), exports);

335
node_modules/bullmq/dist/cjs/classes/ioredis-client.js generated vendored Normal file
View File

@@ -0,0 +1,335 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createIORedisClient = createIORedisClient;
exports.isIRedisClient = isIRedisClient;
/**
* Per-raw-client cache so repeated calls to `createIORedisClient` with the
* same underlying ioredis instance return the same proxy. This preserves
* event-listener identity for the BullMQ-facing client.
*/
const proxyCache = new WeakMap();
/**
* Wraps an ioredis `Redis` / `Cluster` instance with a `Proxy` so it conforms
* to {@link IRedisClient}.
*
* For backwards compatibility BullMQ continues to accept a raw `IORedis`
* instance through tehe `connection` option, even though internally it relies
* on the `IRedisClient` adapter interface. The returned proxy:
*
* - exposes `runCommand` (Lua script dispatch by name)
* - exposes structured-options variants of `hset`, `set`, `zrange`,
* `zrevrange`, `xadd`, `xread`, `xtrim`, `scan` (backward-compatible:
* they still accept native ioredis varargs if called that way)
* - returns augmented {@link IRedisTransaction}s from `pipeline()` / `multi()`
* - wraps the result of `duplicate()` in a new proxy
*
* The underlying ioredis instance is **not** mutated. Properties and methods
* not in the override table are forwarded to the raw client via the proxy
* traps, with `this === target` so EventEmitter / Commander internals work
* normally.
*/
function createIORedisClient(client) {
// If the caller already passed a proxy produced by this function, return
// it as-is. Wrapping a proxy in a second proxy would defeat the WeakMap
// cache (the inner raw client is no longer reachable from the outer
// argument) and break listener-identity / equality checks for callers
// that hold on to the original wrapper.
if (client.__bullmq_iredis === true) {
return client;
}
const cached = proxyCache.get(client);
if (cached) {
return cached;
}
const isCluster = client.isCluster === true;
// Cache bound prototype methods so the returned function identity is
// stable across accesses (important for `once`/`removeListener` patterns).
const boundCache = new Map();
// Override table — properties returned by the proxy without touching the
// underlying ioredis instance. The arrow functions close over `client`
// directly so ioredis internals always see the raw instance as `this`.
const overrides = Object.create(null);
overrides.__bullmq_iredis = true;
overrides.isCluster = isCluster;
// Lua script engine.
overrides.runCommand = (name, args) => {
return client[name](args);
};
// Pipeline / Multi — wrap the ChainableCommander with structured overrides.
overrides.pipeline = (...args) => {
return augmentTransaction(client.pipeline(...args));
};
overrides.multi = (...args) => {
return augmentTransaction(client.multi(...args));
};
// duplicate — wrap the new raw client with a fresh proxy.
// ioredis Cluster.duplicate(startupNodes?, options?) expects connection
// options under `redisOptions`, while Redis.duplicate(options?) takes them
// at the top level. Normalise so callers can always pass `{ connectionName }`.
if (typeof client.duplicate === 'function') {
overrides.duplicate = (opts) => {
var _a;
if (isCluster) {
const existingRedisOpts = ((_a = client.options) === null || _a === void 0 ? void 0 : _a.redisOptions) || {};
const mergedRedisOpts = opts
? Object.assign(Object.assign({}, existingRedisOpts), opts) : existingRedisOpts;
return createIORedisClient(client.duplicate(undefined, {
redisOptions: mergedRedisOpts,
}));
}
return createIORedisClient(client.duplicate(opts));
};
}
// --- Structured → ioredis varargs translations ---
// Each override accepts both the IRedisClient structured-options form and
// the native ioredis varargs form, dispatching by argument shape.
// hset: structured { f1: v1 } → ioredis hset(key, f1, v1, …)
overrides.hset = (key, dataOrField, ...rest) => {
if (typeof dataOrField === 'string') {
return client.hset(key, dataOrField, ...rest);
}
const args = [key];
for (const [f, v] of Object.entries(dataOrField)) {
args.push(f, v);
}
return client.hset(...args);
};
// set: structured { PX?: n } → ioredis set(key, value, 'PX', n)
overrides.set = (key, value, optionsOrModifier, ...rest) => {
if (typeof optionsOrModifier === 'string' || optionsOrModifier == null) {
return client.set(key, value, ...(optionsOrModifier != null ? [optionsOrModifier, ...rest] : []));
}
const args = [key, value];
if (optionsOrModifier.PX != null) {
args.push('PX', optionsOrModifier.PX);
}
else if (optionsOrModifier.EX != null) {
args.push('EX', optionsOrModifier.EX);
}
return client.set(...args);
};
// zrange: structured { WITHSCORES? } → ioredis zrange(key, start, end, 'WITHSCORES')
overrides.zrange = (key, start, end, optionsOrStr, ...rest) => {
if (typeof optionsOrStr === 'string') {
return client.zrange(key, start, end, optionsOrStr, ...rest);
}
if (optionsOrStr === null || optionsOrStr === void 0 ? void 0 : optionsOrStr.WITHSCORES) {
return client.zrange(key, start, end, 'WITHSCORES');
}
return client.zrange(key, start, end);
};
// zrevrange: structured { WITHSCORES? } → ioredis zrevrange(key, start, end, 'WITHSCORES')
overrides.zrevrange = (key, start, end, optionsOrStr, ...rest) => {
if (typeof optionsOrStr === 'string') {
return client.zrevrange(key, start, end, optionsOrStr, ...rest);
}
if (optionsOrStr === null || optionsOrStr === void 0 ? void 0 : optionsOrStr.WITHSCORES) {
return client.zrevrange(key, start, end, 'WITHSCORES');
}
return client.zrevrange(key, start, end);
};
// xadd: structured (key, id, { field: value }, { MAXLEN? }) → ioredis varargs
overrides.xadd = (key, idOrModifier, fieldsOrArg, ...rest) => {
if (typeof fieldsOrArg === 'string') {
return client.xadd(key, idOrModifier, fieldsOrArg, ...rest);
}
const options = rest[0];
const args = [key];
if ((options === null || options === void 0 ? void 0 : options.MAXLEN) != null) {
args.push('MAXLEN');
if (options.approximate !== false) {
args.push('~');
}
args.push(options.MAXLEN);
}
args.push(idOrModifier);
for (const [f, v] of Object.entries(fieldsOrArg)) {
args.push(f, v);
}
return client.xadd(...args);
};
// xread: structured ([{ key, id }], { BLOCK?, COUNT? }) → ioredis varargs
overrides.xread = (streamsOrModifier, ...rest) => {
if (typeof streamsOrModifier === 'string') {
return client.xread(streamsOrModifier, ...rest);
}
const options = rest[0];
const args = [];
if ((options === null || options === void 0 ? void 0 : options.BLOCK) != null) {
args.push('BLOCK', options.BLOCK);
}
if ((options === null || options === void 0 ? void 0 : options.COUNT) != null) {
args.push('COUNT', options.COUNT);
}
args.push('STREAMS');
for (const s of streamsOrModifier) {
args.push(s.key);
}
for (const s of streamsOrModifier) {
args.push(s.id);
}
return client.xread(...args);
};
// xtrim: structured (key, 'MAXLEN', threshold, { approximate? })
overrides.xtrim = (key, strategy, thresholdOrApprox, ...rest) => {
if (typeof thresholdOrApprox === 'string' || rest.length === 0) {
return client.xtrim(key, strategy, thresholdOrApprox, ...rest);
}
const options = rest[0];
const args = [key, strategy];
if ((options === null || options === void 0 ? void 0 : options.approximate) !== false) {
args.push('~');
}
args.push(thresholdOrApprox);
return client.xtrim(...args);
};
// bzpopmin is not overridden — ioredis already returns
// `[key, member, score]`, which matches IRedisClient.
// clientSetName / clientList helpers that forward to CLIENT subcommands.
overrides.clientSetName = (name) => client.client('SETNAME', name);
overrides.clientList = () => client.client('LIST');
// scan(cursor, { MATCH?, COUNT? }) — accepts either structured options or
// ioredis varargs (used internally by `scanStream`).
overrides.scan = (cursor, ...rest) => {
if (rest.length === 0 ||
typeof rest[0] === 'string' ||
typeof rest[0] === 'function') {
return client.scan(cursor, ...rest);
}
const options = rest[0];
const args = [cursor];
if ((options === null || options === void 0 ? void 0 : options.MATCH) != null) {
args.push('MATCH', options.MATCH);
}
if ((options === null || options === void 0 ? void 0 : options.COUNT) != null) {
args.push('COUNT', options.COUNT);
}
return client.scan(...args);
};
const proxy = new Proxy(client, {
get(target, prop) {
if (prop in overrides) {
return overrides[prop];
}
// Read against the raw target so getters on the prototype (e.g.
// ioredis' EventEmitter internals) see `this === target` rather than
// the proxy. This avoids infinite recursion through the proxy traps.
const value = Reflect.get(target, prop, target);
if (typeof value !== 'function') {
return value;
}
// Own properties (including ioredis commands installed via
// `defineCommand` and test-time spies set via `obj.method = spy`)
// are bound fresh on each access so reassignment is honoured.
if (Object.prototype.hasOwnProperty.call(target, prop)) {
return value.bind(target);
}
// Prototype methods (EventEmitter, Commander, ...) are cached so
// identity is stable across accesses.
const cachedBound = boundCache.get(prop);
if (cachedBound !== undefined) {
return cachedBound;
}
const bound = value.bind(target);
boundCache.set(prop, bound);
return bound;
},
set(target, prop, value) {
// Two assignment paths:
// - Properties present in the override table are reassigned in the
// table itself, so subsequent `get` traps return the new value
// (used by sinon-style spies that stub `runCommand`, `pipeline`,
// etc. on the proxy).
// - All other properties are written through to the raw ioredis
// instance via `Reflect.set`, and any stale bound-method entry is
// invalidated so the next access rebinds the new function.
if (prop in overrides) {
overrides[prop] = value;
return true;
}
boundCache.delete(prop);
return Reflect.set(target, prop, value);
},
deleteProperty(target, prop) {
if (prop in overrides) {
return false;
}
boundCache.delete(prop);
return Reflect.deleteProperty(target, prop);
},
has(target, prop) {
return prop in overrides || Reflect.has(target, prop);
},
});
proxyCache.set(client, proxy);
return proxy;
}
/**
* Adds `runCommand` and structured overrides to an ioredis ChainableCommander
* so it satisfies {@link IRedisTransaction}.
*/
function augmentTransaction(commander) {
const transaction = commander;
transaction.runCommand = function (name, args) {
transaction[name](args);
return transaction;
};
// hset(key, { f1: v1 }) → ioredis pipeline.hset(key, f1, v1, …)
const origHset = transaction.hset.bind(transaction);
transaction.hset = function (key, data) {
const args = [key];
for (const [f, v] of Object.entries(data)) {
args.push(f, v);
}
origHset(...args);
return transaction;
};
// hscan(key, cursor, { COUNT? }) → ioredis hscan(key, cursor, 'COUNT', n)
const origHscan = transaction.hscan.bind(transaction);
transaction.hscan = function (key, cursor, options) {
if ((options === null || options === void 0 ? void 0 : options.COUNT) != null) {
origHscan(key, cursor, 'COUNT', options.COUNT);
}
else {
origHscan(key, cursor);
}
return transaction;
};
// sscan(key, cursor, { COUNT? })
const origSscan = transaction.sscan.bind(transaction);
transaction.sscan = function (key, cursor, options) {
if ((options === null || options === void 0 ? void 0 : options.COUNT) != null) {
origSscan(key, cursor, 'COUNT', options.COUNT);
}
else {
origSscan(key, cursor);
}
return transaction;
};
return transaction;
}
/**
* Check if an object already implements {@link IRedisClient}.
*/
function isIRedisClient(obj) {
if (!obj || typeof obj !== 'object') {
return false;
}
// Fast path for ioredis instances already wrapped by `createIORedisClient`.
if (obj.__bullmq_iredis === true) {
return true;
}
// Fallback structural check for wrapper-based adapters
// (node-redis, Bun, or custom IRedisClient implementations).
return (typeof obj.runCommand === 'function' &&
typeof obj.defineCommand === 'function' &&
typeof obj.pipeline === 'function' &&
typeof obj.multi === 'function' &&
typeof obj.duplicate === 'function' &&
typeof obj.scanStream === 'function' &&
typeof obj.connect === 'function' &&
typeof obj.disconnect === 'function' &&
typeof obj.on === 'function' &&
typeof obj.status === 'string' &&
typeof obj.isCluster === 'boolean');
}

347
node_modules/bullmq/dist/cjs/classes/job-scheduler.js generated vendored Normal file
View File

@@ -0,0 +1,347 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getNextMillis = exports.defaultRepeatStrategy = exports.JobScheduler = exports.isLegacyRepeatableJobKey = exports.LEGACY_REPEATABLE_JOBS_MIGRATION_URL = void 0;
exports.hasLegacyRepeatableKeyShape = hasLegacyRepeatableKeyShape;
exports.getLegacyRepeatableJobError = getLegacyRepeatableJobError;
const tslib_1 = require("tslib");
const cron_parser_1 = require("cron-parser");
const job_1 = require("./job");
const queue_base_1 = require("./queue-base");
const enums_1 = require("../enums");
const utils_1 = require("../utils");
exports.LEGACY_REPEATABLE_JOBS_MIGRATION_URL = 'https://docs.bullmq.io/guide/migrations/migrate-from-v5-to-v6';
/**
* Legacy repeatable job keys use the format `name:id:endDate:tz:pattern`.
* The suffix after `name:id:endDate:tz:` is either:
* - a cron pattern (contains spaces), or
* - an `every` interval (purely numeric).
*/
/**
* Returns true when key[from..to) is composed only of ASCII digits.
*/
function isNumericSegment(key, from, to) {
if (from >= to) {
return false;
}
for (let i = from; i < to; i++) {
const charCode = key.charCodeAt(i);
if (charCode < 48 || charCode > 57) {
return false;
}
}
return true;
}
function hasLegacyRepeatableKeyShape(key) {
const firstColon = key.indexOf(':');
if (firstColon === -1) {
return false;
}
const secondColon = key.indexOf(':', firstColon + 1);
if (secondColon === -1) {
return false;
}
const thirdColon = key.indexOf(':', secondColon + 1);
if (thirdColon === -1) {
return false;
}
const fourthColon = key.indexOf(':', thirdColon + 1);
if (fourthColon === -1) {
return false;
}
// endDate can be empty or numeric in legacy keys.
if (secondColon + 1 < thirdColon &&
!isNumericSegment(key, secondColon + 1, thirdColon)) {
return false;
}
const suffixStart = fourthColon + 1;
if (suffixStart >= key.length) {
return false;
}
if (key.indexOf(' ', suffixStart) !== -1) {
return true;
}
return isNumericSegment(key, suffixStart, key.length);
}
exports.isLegacyRepeatableJobKey = hasLegacyRepeatableKeyShape;
function getLegacyRepeatableJobError(key) {
return new Error(`Legacy repeatable job metadata is not supported in BullMQ v6 ` +
`(key: "${key}"). Migrate legacy repeatable jobs to Job Schedulers ` +
`before upgrading. See ${exports.LEGACY_REPEATABLE_JOBS_MIGRATION_URL}`);
}
class JobScheduler extends queue_base_1.QueueBase {
constructor(name, opts, backendFactory) {
super(name, opts, backendFactory);
this.repeatStrategy =
(opts.settings && opts.settings.repeatStrategy) || exports.defaultRepeatStrategy;
}
async upsertJobScheduler(jobSchedulerId, repeatOpts, jobName, jobData, opts, { override, producerId }) {
const { every, limit, pattern, offset } = repeatOpts;
if (pattern && every) {
throw new Error('Both .pattern and .every options are defined for this repeatable job');
}
if (!pattern && !every) {
throw new Error('Either .pattern or .every options must be defined for this repeatable job');
}
if (repeatOpts.immediately && repeatOpts.startDate) {
throw new Error('Both .immediately and .startDate options are defined for this repeatable job');
}
if (repeatOpts.immediately && repeatOpts.every) {
console.warn("Using option immediately with every does not affect the job's schedule. Job will run immediately anyway.");
}
if (Object.prototype.hasOwnProperty.call(opts, 'debounce')) {
throw new Error('Debounce option has been removed. Use deduplication option instead');
}
// Check if we reached the limit of the repeatable job's iterations
const iterationCount = repeatOpts.count ? repeatOpts.count + 1 : 1;
if (typeof repeatOpts.limit !== 'undefined' &&
iterationCount > repeatOpts.limit) {
return;
}
// Check if we reached the end date of the repeatable job
let now = Date.now();
const { endDate } = repeatOpts;
if (endDate && now > new Date(endDate).getTime()) {
return;
}
const prevMillis = opts.prevMillis || 0;
now = prevMillis < now ? now : prevMillis;
// Check if we have a start date for the repeatable job
const { immediately } = repeatOpts, filteredRepeatOpts = tslib_1.__rest(repeatOpts, ["immediately"]);
let nextMillis;
const newOffset = every && offset ? offset : null;
if (pattern) {
nextMillis = await this.repeatStrategy(now, repeatOpts, jobName);
if (nextMillis < now) {
nextMillis = now;
}
}
if (nextMillis || every) {
return this.trace(enums_1.SpanKind.PRODUCER, 'add', `${this.name}.${jobName}`, async (span, srcPropagationMetadata) => {
var _a, _b;
let telemetry = opts.telemetry;
if (srcPropagationMetadata) {
const omitContext = (_a = opts.telemetry) === null || _a === void 0 ? void 0 : _a.omitContext;
const telemetryMetadata = ((_b = opts.telemetry) === null || _b === void 0 ? void 0 : _b.metadata) ||
(!omitContext && srcPropagationMetadata);
if (telemetryMetadata || omitContext) {
telemetry = {
metadata: telemetryMetadata,
omitContext,
};
}
}
const mergedOpts = this.getNextJobOpts(nextMillis, jobSchedulerId, Object.assign(Object.assign({}, opts), { repeat: filteredRepeatOpts, telemetry }), iterationCount, newOffset);
if (override) {
// Clamp nextMillis to now if it's in the past
if (nextMillis < now) {
nextMillis = now;
}
const [jobId, delay] = await this.backend.addJobScheduler(jobSchedulerId, nextMillis, JSON.stringify(typeof jobData === 'undefined' ? {} : jobData), opts, {
name: jobName,
startDate: repeatOpts.startDate
? new Date(repeatOpts.startDate).getTime()
: undefined,
endDate: endDate ? new Date(endDate).getTime() : undefined,
tz: repeatOpts.tz,
pattern,
every,
limit,
offset: newOffset,
}, mergedOpts, producerId);
// Ensure delay is a number (Dragonflydb may return it as a string)
const numericDelay = typeof delay === 'string' ? parseInt(delay, 10) : delay;
const job = new this.Job(this, jobName, jobData, Object.assign(Object.assign({}, mergedOpts), { delay: numericDelay }), jobId);
job.id = jobId;
span === null || span === void 0 ? void 0 : span.setAttributes({
[enums_1.TelemetryAttributes.JobSchedulerId]: jobSchedulerId,
[enums_1.TelemetryAttributes.JobId]: job.id,
});
return job;
}
else {
const jobId = await this.backend.updateJobSchedulerNextMillis(jobSchedulerId, nextMillis, JSON.stringify(typeof jobData === 'undefined' ? {} : jobData), mergedOpts, producerId);
if (jobId) {
const job = new this.Job(this, jobName, jobData, mergedOpts, jobId);
job.id = jobId;
span === null || span === void 0 ? void 0 : span.setAttributes({
[enums_1.TelemetryAttributes.JobSchedulerId]: jobSchedulerId,
[enums_1.TelemetryAttributes.JobId]: job.id,
});
return job;
}
}
});
}
}
getNextJobOpts(nextMillis, jobSchedulerId, opts, currentCount, offset) {
var _a, _b;
//
// Generate unique job id for this iteration.
//
const jobId = this.getSchedulerNextJobId({
jobSchedulerId,
nextMillis,
});
const now = Date.now();
const delay = nextMillis + offset - now;
const mergedOpts = Object.assign(Object.assign({}, opts), { jobId, delay: delay < 0 ? 0 : delay, timestamp: now, prevMillis: nextMillis, repeatJobKey: jobSchedulerId });
mergedOpts.repeat = Object.assign(Object.assign({}, opts.repeat), { offset, count: currentCount, startDate: ((_a = opts.repeat) === null || _a === void 0 ? void 0 : _a.startDate)
? new Date(opts.repeat.startDate).getTime()
: undefined, endDate: ((_b = opts.repeat) === null || _b === void 0 ? void 0 : _b.endDate)
? new Date(opts.repeat.endDate).getTime()
: undefined });
return mergedOpts;
}
async removeJobScheduler(jobSchedulerId) {
return this.backend.removeJobScheduler(jobSchedulerId);
}
async getSchedulerData(key, next) {
const jobData = await this.backend.getJobSchedulerData(key);
const scheduler = this.transformSchedulerData(key, jobData, next);
if (!scheduler) {
await this.backend.removeJobScheduler(key);
}
return scheduler;
}
transformSchedulerData(key, jobData, next) {
if (jobData && Object.keys(jobData).length > 0) {
const jobSchedulerData = {
key,
name: jobData.name,
next,
};
if (jobData.ic) {
jobSchedulerData.iterationCount = parseInt(jobData.ic);
}
if (jobData.limit) {
jobSchedulerData.limit = parseInt(jobData.limit);
}
if (jobData.startDate) {
jobSchedulerData.startDate = parseInt(jobData.startDate);
}
if (jobData.endDate) {
jobSchedulerData.endDate = parseInt(jobData.endDate);
}
if (jobData.tz) {
jobSchedulerData.tz = jobData.tz;
}
if (jobData.pattern) {
jobSchedulerData.pattern = jobData.pattern;
}
if (jobData.every) {
jobSchedulerData.every = parseInt(jobData.every);
}
if (jobData.offset) {
jobSchedulerData.offset = parseInt(jobData.offset);
}
if (jobData.data || jobData.opts) {
jobSchedulerData.template = this.getTemplateFromJSON(jobData.data, jobData.opts);
}
return jobSchedulerData;
}
if (hasLegacyRepeatableKeyShape(key)) {
throw getLegacyRepeatableJobError(key);
}
return undefined;
}
/**
* Checks if a given id corresponds to a registered job scheduler.
*
* This is used to disambiguate between new job scheduler ids (which may
* contain any number of colon segments) and legacy repeatable job keys
* (which always contain 5+ colon segments). Relying purely on segment
* count is not safe because a user-provided jobSchedulerId may itself
* contain 5+ colon segments, which would otherwise be misclassified as
* a legacy repeatable key.
*
* We cannot use ZSCORE on the shared `repeat` sorted set because legacy
* repeatable jobs are stored in the same sorted set and would be reported
* as schedulers. Instead, we probe the per-id metadata hash (`repeat:<id>`)
* for the `ic` (iteration count) field, which is written exclusively by
* `storeJobScheduler` and is never set by the legacy `addRepeatableJob`
* flow.
*/
async isJobScheduler(id) {
return this.backend.isJobScheduler(id);
}
async getScheduler(id) {
const [rawJobData, next] = await this.backend.getJobScheduler(id);
return this.transformSchedulerData(id, rawJobData ? (0, utils_1.array2obj)(rawJobData) : null, next ? parseInt(next) : null);
}
getTemplateFromJSON(rawData, rawOpts) {
const template = {};
if (rawData) {
template.data = JSON.parse(rawData);
}
if (rawOpts) {
template.opts = job_1.Job.optsFromJSON(rawOpts);
}
return template;
}
async getJobSchedulers(start = 0, end = -1, asc = false) {
const result = await this.backend.getJobSchedulersRange(start, end, asc);
const jobs = [];
for (let i = 0; i < result.length; i += 2) {
jobs.push(this.getSchedulerData(result[i], parseInt(result[i + 1])));
}
return (await Promise.all(jobs)).filter((job) => !!job);
}
async getSchedulersCount() {
return this.backend.getJobSchedulersCount();
}
getSchedulerNextJobId({ nextMillis, jobSchedulerId, }) {
return `repeat:${jobSchedulerId}:${nextMillis}`;
}
}
exports.JobScheduler = JobScheduler;
const defaultRepeatStrategy = (millis, opts) => {
const { pattern } = opts;
const dateFromMillis = new Date(millis);
const startDate = opts.startDate && new Date(opts.startDate);
const currentDate = startDate > dateFromMillis ? startDate : dateFromMillis;
const interval = cron_parser_1.CronExpressionParser.parse(pattern, Object.assign(Object.assign({}, opts), { currentDate }));
try {
if (opts.immediately) {
return new Date().getTime();
}
else {
return interval.next().getTime();
}
}
catch (e) {
// Ignore error
}
};
exports.defaultRepeatStrategy = defaultRepeatStrategy;
/**
* Computes the next execution time (in ms since epoch) for the given repeat
* options, supporting both `.every` (fixed interval) and `.pattern` (cron)
* strategies. This is the default repeat strategy used to schedule the next
* iteration of a job scheduler.
*/
const getNextMillis = (millis, opts) => {
const pattern = opts.pattern;
if (pattern && opts.every) {
throw new Error('Both .pattern and .every options are defined for this repeatable job');
}
if (opts.every) {
return (Math.floor(millis / opts.every) * opts.every +
(opts.immediately ? 0 : opts.every));
}
const currentDate = opts.startDate && new Date(opts.startDate) > new Date(millis)
? new Date(opts.startDate)
: new Date(millis);
const interval = cron_parser_1.CronExpressionParser.parse(pattern, Object.assign(Object.assign({}, opts), { currentDate }));
try {
if (opts.immediately) {
return new Date().getTime();
}
else {
return interval.next().getTime();
}
}
catch (e) {
// Ignore error
}
};
exports.getNextMillis = getNextMillis;

981
node_modules/bullmq/dist/cjs/classes/job.js generated vendored Normal file
View File

@@ -0,0 +1,981 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Job = exports.PRIORITY_LIMIT = void 0;
const tslib_1 = require("tslib");
const util_1 = require("util");
const utils_1 = require("../utils");
const backoffs_1 = require("./backoffs");
const unrecoverable_error_1 = require("./errors/unrecoverable-error");
const enums_1 = require("../enums");
const logger = (0, util_1.debuglog)('bull');
exports.PRIORITY_LIMIT = 2 ** 21 - 1;
/**
* Job
*
* This class represents a Job in the queue. Normally job are implicitly created when
* you add a job to the queue with methods such as Queue.addJob( ... )
*
* A Job instance is also passed to the Worker's process function.
*
*/
class Job {
constructor(queue,
/**
* The name of the Job
*/
name,
/**
* The payload for this job.
*/
data,
/**
* The options object for this job.
*/
opts = {}, id) {
var _a;
this.queue = queue;
this.name = name;
this.data = data;
this.opts = opts;
this.id = id;
/**
* The progress a job has performed so far.
* @defaultValue 0
*/
this.progress = 0;
/**
* The value returned by the processor when processing this job.
* @defaultValue null
*/
this.returnvalue = null;
/**
* Stacktrace for the error (for failed jobs).
* @defaultValue null
*/
this.stacktrace = null;
/**
* An amount of milliseconds to wait until this job can be processed.
* @defaultValue 0
*/
this.delay = 0;
/**
* Ranges from 0 to 2 097 151. `0` means no explicit priority, and jobs with
* no explicit priority are processed before prioritized jobs. For prioritized
* jobs, lower numbers are processed before higher numbers. Note that using
* priorities has a slight impact on performance, so do not use it if not
* required.
* @defaultValue 0
*/
this.priority = 0;
/**
* Number of attempts when job is moved to active.
* @defaultValue 0
*/
this.attemptsStarted = 0;
/**
* Number of attempts after the job has failed.
* @defaultValue 0
*/
this.attemptsMade = 0;
/**
* Number of times where job has stalled.
* @defaultValue 0
*/
this.stalledCounter = 0;
const _b = this.opts, { repeatJobKey } = _b, restOpts = tslib_1.__rest(_b, ["repeatJobKey"]);
this.opts = Object.assign({
attempts: 0,
}, restOpts);
this.delay = this.opts.delay;
this.priority = this.opts.priority || 0;
this.repeatJobKey = repeatJobKey;
this.timestamp = opts.timestamp ? opts.timestamp : Date.now();
this.opts.backoff = backoffs_1.Backoffs.normalize(opts.backoff);
this.parentKey = (0, utils_1.getParentKey)(opts.parent);
if (opts.parent) {
this.parent = { id: opts.parent.id, queueKey: opts.parent.queue };
if (opts.failParentOnFailure) {
this.parent.fpof = true;
}
if (opts.removeDependencyOnFailure) {
this.parent.rdof = true;
}
if (opts.ignoreDependencyOnFailure) {
this.parent.idof = true;
}
if (opts.continueParentOnFailure) {
this.parent.cpof = true;
}
}
this.deduplicationId = (_a = this.opts.deduplication) === null || _a === void 0 ? void 0 : _a.id;
this.toKey = queue.toKey.bind(queue);
this.createBackend();
this.queueQualifiedName = queue.qualifiedName;
}
/**
* Creates a new job and adds it to the queue.
*
* @param queue - the queue where to add the job.
* @param name - the name of the job.
* @param data - the payload of the job.
* @param opts - the options bag for this job.
* @returns The created Job instance
*/
static async create(queue, name, data, opts) {
const job = new this(queue, name, data, opts, opts && opts.jobId);
const jobData = job.asJSON();
job.validateOptions(jobData);
job.id = await job.backend.addJob(jobData, job.id, {
parentKey: job.parentKey,
parentDependenciesKey: job.parentKey
? `${job.parentKey}:dependencies`
: '',
});
return job;
}
/**
* Creates a bulk of jobs and adds them atomically to the given queue.
*
* @param queue - the queue where to add the jobs.
* @param jobs - an array of jobs to be added to the queue.
* @returns The created Job instances
*/
static async createBulk(queue, jobs) {
const jobInstances = jobs.map(job => { var _a; return new this(queue, job.name, job.data, job.opts, (_a = job.opts) === null || _a === void 0 ? void 0 : _a.jobId); });
const scripts = queue.backend;
const entries = jobInstances.map(job => {
const jobData = job.asJSON();
job.validateOptions(jobData);
return {
job: jobData,
jobId: job.id,
parentKeyOpts: {
parentKey: job.parentKey,
parentDependenciesKey: job.parentKey
? `${job.parentKey}:dependencies`
: '',
},
};
});
const ids = await scripts.addJobs(entries);
jobInstances.forEach((job, index) => {
job.id = ids[index];
});
return jobInstances;
}
/**
* Instantiates a Job from a {@link JobJson} object (the public, decoded
* representation of a stored job).
*
* @param queue - the queue where the job belongs to.
* @param json - the plain object containing the job.
* @param jobId - an optional job id (overrides the id coming from the JSON object)
* @returns A Job instance reconstructed from the JSON data
*/
static fromJSON(queue, json, jobId) {
var _a, _b, _c, _d;
const data = JSON.parse(json.data || '{}');
const job = new this(queue, json.name, data, json.opts, json.id || jobId);
job.progress = (_a = json.progress) !== null && _a !== void 0 ? _a : 0;
job.delay = json.delay;
job.priority = json.priority;
job.timestamp = json.timestamp;
if (json.finishedOn) {
job.finishedOn = json.finishedOn;
}
if (json.processedOn) {
job.processedOn = json.processedOn;
}
if (json.repeatJobKey) {
job.repeatJobKey = json.repeatJobKey;
}
if (json.deduplicationId) {
job.deduplicationId = json.deduplicationId;
}
if (json.failedReason) {
job.failedReason = json.failedReason;
}
job.attemptsStarted = (_b = json.attemptsStarted) !== null && _b !== void 0 ? _b : 0;
job.attemptsMade = (_c = json.attemptsMade) !== null && _c !== void 0 ? _c : 0;
job.stalledCounter = (_d = json.stalledCounter) !== null && _d !== void 0 ? _d : 0;
if (json.deferredFailure) {
job.deferredFailure = json.deferredFailure;
}
job.stacktrace = getTraces(json.stacktrace);
if (typeof json.returnvalue === 'string') {
job.returnvalue = getReturnValue(json.returnvalue);
}
if (json.parentKey) {
job.parentKey = json.parentKey;
}
else {
job.parentKey = undefined;
}
if (json.parent) {
job.parent = json.parent;
}
else {
job.parent = undefined;
}
if (json.processedBy) {
job.processedBy = json.processedBy;
}
return job;
}
createBackend() {
this.backend = this.queue.backend;
}
static optsFromJSON(rawOpts, optsDecode = utils_1.optsDecodeMap) {
const opts = JSON.parse(rawOpts || '{}');
const optionEntries = Object.entries(opts);
const options = {};
for (const item of optionEntries) {
const [attributeName, value] = item;
if (optsDecode[attributeName]) {
options[optsDecode[attributeName]] =
value;
}
else {
if (attributeName === 'tm') {
options.telemetry = Object.assign(Object.assign({}, options.telemetry), { metadata: value });
}
else if (attributeName === 'omc') {
options.telemetry = Object.assign(Object.assign({}, options.telemetry), { omitContext: value });
}
else {
options[attributeName] = value;
}
}
}
return options;
}
/**
* Fetches a Job from the queue given the passed job id.
*
* @param queue - the queue where the job belongs to.
* @param jobId - the job id.
* @returns
*/
static async fromId(queue, jobId) {
// jobId can be undefined if moveJob returns undefined
if (jobId) {
const scripts = queue.backend;
const jobData = await scripts.getJobData(jobId);
return jobData
? this.fromJSON(queue, jobData, jobId)
: undefined;
}
}
/**
* addJobLog
*
* @param queue - A minimal queue instance
* @param jobId - Job id
* @param logRow - String with a row of log data to be logged
* @param keepLogs - The optional amount of log entries to preserve
*
* @returns The total number of log entries for this job so far.
*/
static addJobLog(queue, jobId, logRow, keepLogs) {
const scripts = queue.backend;
return scripts.addLog(jobId, logRow, keepLogs);
}
toJSON() {
const _a = this, { queue, backend: scripts } = _a, withoutQueueAndScripts = tslib_1.__rest(_a, ["queue", "backend"]);
return withoutQueueAndScripts;
}
/**
* Prepares a job to be serialized for storage in Redis.
* @returns
*/
asJSON() {
return (0, utils_1.removeUndefinedFields)({
id: this.id,
name: this.name,
data: JSON.stringify(typeof this.data === 'undefined' ? {} : this.data),
opts: this.opts,
parent: this.parent ? Object.assign({}, this.parent) : undefined,
parentKey: this.parentKey,
progress: this.progress,
attemptsMade: this.attemptsMade,
attemptsStarted: this.attemptsStarted,
stalledCounter: this.stalledCounter,
finishedOn: this.finishedOn,
processedOn: this.processedOn,
timestamp: this.timestamp,
failedReason: JSON.stringify(this.failedReason),
stacktrace: JSON.stringify(this.stacktrace),
deduplicationId: this.deduplicationId,
repeatJobKey: this.repeatJobKey,
returnvalue: JSON.stringify(this.returnvalue),
});
}
/**
* Prepares a job to be passed to Sandbox.
* @returns
*/
asJSONSandbox() {
return Object.assign(Object.assign({}, this.asJSON()), { queueName: this.queueName, queueQualifiedName: this.queueQualifiedName, prefix: this.prefix });
}
/**
* Updates a job's data
*
* @param data - the data that will replace the current jobs data.
*/
updateData(data) {
this.data = data;
return this.backend.updateData(this, data);
}
/**
* Updates a job's progress
*
* @param progress - number or object to be saved as progress.
*/
async updateProgress(progress) {
this.progress = progress;
await this.backend.updateProgress(this.id, progress);
this.queue.emit('progress', this, progress);
}
/**
* Logs one row of log data.
*
* @param logRow - string with log data to be logged.
* @returns The total number of log entries for this job so far.
*/
async log(logRow) {
return Job.addJobLog(this.queue, this.id, logRow, this.opts.keepLogs);
}
/**
* Removes child dependency from parent when child is not yet finished
*
* @returns True if the relationship existed and if it was removed.
*/
async removeChildDependency() {
const childDependencyIsRemoved = await this.backend.removeChildDependency(this.id, this.parentKey);
if (childDependencyIsRemoved) {
this.parent = undefined;
this.parentKey = undefined;
return true;
}
return false;
}
/**
* Clears job's logs
*
* @param keepLogs - the amount of log entries to preserve
*/
async clearLogs(keepLogs) {
await this.backend.clearLogs(this.id, keepLogs);
}
/**
* Completely remove the job from the queue.
* Note, this call will throw an exception if the job
* is being processed when the call is performed.
*
* @param opts - Options to remove a job
*/
async remove({ removeChildren = true } = {}) {
await this.queue.waitUntilReady();
const queue = this.queue;
const job = this;
const removed = await this.backend.remove(job.id, removeChildren);
if (removed) {
queue.emit('removed', job);
}
else {
throw new Error(`Job ${this.id} could not be removed because it is locked by another worker`);
}
}
/**
* Remove all children from this job that are not yet processed,
* in other words that are in any other state than completed, failed or active.
*
* @remarks
* - Jobs with locks (most likely active) are ignored.
* - This method can be slow if the number of children is large (\> 1000).
*/
async removeUnprocessedChildren() {
const jobId = this.id;
await this.backend.removeUnprocessedChildren(jobId);
}
/**
* Extend the lock for this job.
*
* @param token - unique token for the lock
* @param duration - lock duration in milliseconds
*/
extendLock(token, duration) {
return this.backend.extendLock(this.id, token, duration);
}
/**
* Moves a job to the completed queue.
* Returned job to be used with Queue.prototype.nextJobFromJobData.
*
* @param returnValue - The jobs success message.
* @param token - Worker token used to acquire completed job.
* @param fetchNext - True when wanting to fetch the next job.
* @returns Returns the jobData of the next job in the waiting queue or void.
*/
async moveToCompleted(returnValue, token, fetchNext = true) {
return this.queue.trace(enums_1.SpanKind.INTERNAL, 'complete', this.queue.name, async (span) => {
this.setSpanJobAttributes(span);
await this.queue.waitUntilReady();
this.returnvalue = returnValue || void 0;
const stringifiedReturnValue = (0, utils_1.tryCatch)(JSON.stringify, JSON, [
returnValue,
]);
if (stringifiedReturnValue === utils_1.errorObject) {
throw utils_1.errorObject.value;
}
const { result, finishedOn } = await this.backend.moveToCompleted(this, returnValue, this.opts.removeOnComplete, token, fetchNext);
this.finishedOn = finishedOn;
this.attemptsMade += 1;
this.recordJobMetrics('completed');
return result;
});
}
/**
* Moves a job to the wait or prioritized state.
*
* @param token - Worker token used to acquire completed job.
* @returns Returns pttl.
*/
async moveToWait(token) {
const result = await this.backend.moveJobFromActiveToWait(this.id, token);
this.recordJobMetrics('waiting');
return result;
}
async shouldRetryJob(err) {
if (this.attemptsMade + 1 < this.opts.attempts &&
!(err instanceof unrecoverable_error_1.UnrecoverableError || err.name == 'UnrecoverableError')) {
const opts = this.queue.opts;
const delay = await backoffs_1.Backoffs.calculate(this.opts.backoff, this.attemptsMade + 1, err, this, opts.settings && opts.settings.backoffStrategy);
return [delay == -1 ? false : true, delay == -1 ? 0 : delay];
}
else {
return [false, 0];
}
}
/**
* Moves a job to the failed queue.
*
* @param err - the jobs error message.
* @param token - token to check job is locked by current worker
* @param fetchNext - true when wanting to fetch the next job
* @returns Returns the jobData of the next job in the waiting queue or void.
*/
async moveToFailed(err, token, fetchNext = false) {
this.failedReason = err === null || err === void 0 ? void 0 : err.message;
// Check if an automatic retry should be performed
const [shouldRetry, retryDelay] = await this.shouldRetryJob(err);
return this.queue.trace(enums_1.SpanKind.INTERNAL, this.getSpanOperation(shouldRetry, retryDelay), this.queue.name, async (span, dstPropagationMetadata) => {
var _a, _b;
this.setSpanJobAttributes(span);
let tm;
if (!((_b = (_a = this.opts) === null || _a === void 0 ? void 0 : _a.telemetry) === null || _b === void 0 ? void 0 : _b.omitContext) && dstPropagationMetadata) {
tm = dstPropagationMetadata;
}
let result;
this.updateStacktrace(err);
const fieldsToUpdate = {
failedReason: this.failedReason,
stacktrace: JSON.stringify(this.stacktrace),
tm,
};
let finishedOn;
if (shouldRetry) {
if (retryDelay) {
// Retry with delay
result = await this.backend.moveToDelayed(this.id, Date.now(), retryDelay, token, { fieldsToUpdate, fetchNext });
this.recordJobMetrics('delayed');
}
else {
// Retry immediately
result = await this.backend.retryJob(this.id, this.opts.lifo, token, {
fieldsToUpdate,
});
this.recordJobMetrics('retried');
}
}
else {
const moved = await this.backend.moveToFailed(this, this.failedReason, this.opts.removeOnFail, token, fetchNext, fieldsToUpdate);
result = moved.result;
finishedOn = moved.finishedOn;
// Only record failed metrics when job is not retrying
this.recordJobMetrics('failed');
}
if (finishedOn && typeof finishedOn === 'number') {
this.finishedOn = finishedOn;
}
if (retryDelay && typeof retryDelay === 'number') {
this.delay = retryDelay;
}
this.attemptsMade += 1;
return result;
});
}
getSpanOperation(shouldRetry, retryDelay) {
if (shouldRetry) {
if (retryDelay) {
return 'delay';
}
return 'retry';
}
return 'fail';
}
/**
* Records job metrics if a meter is configured in telemetry options.
*
* @param state - The job state
*/
recordJobMetrics(state) {
var _a, _b;
const meter = (_b = (_a = this.queue.opts) === null || _a === void 0 ? void 0 : _a.telemetry) === null || _b === void 0 ? void 0 : _b.meter;
if (!meter) {
return;
}
const attributes = {
[enums_1.TelemetryAttributes.QueueName]: this.queue.name,
[enums_1.TelemetryAttributes.JobName]: this.name,
[enums_1.TelemetryAttributes.JobState]: state,
};
// Record counter metric based on state
const stateToCounterName = {
completed: enums_1.MetricNames.JobsCompleted,
failed: enums_1.MetricNames.JobsFailed,
delayed: enums_1.MetricNames.JobsDelayed,
retried: enums_1.MetricNames.JobsRetried,
waiting: enums_1.MetricNames.JobsWaiting,
'waiting-children': enums_1.MetricNames.JobsWaitingChildren,
};
const counterName = stateToCounterName[state];
const counter = meter.createCounter(counterName, {
description: `Number of jobs ${state}`,
unit: '1',
});
counter.add(1, attributes);
// Record duration histogram if processedOn is available
if (this.processedOn) {
const duration = Date.now() - this.processedOn;
const histogram = meter.createHistogram(enums_1.MetricNames.JobDuration, {
description: 'Job processing duration',
unit: 'ms',
});
histogram.record(duration, attributes);
}
}
/**
* @returns true if the job has completed.
*/
isCompleted() {
return this.isInState('completed');
}
/**
* @returns true if the job has failed.
*/
isFailed() {
return this.isInState('failed');
}
/**
* @returns true if the job is delayed.
*/
isDelayed() {
return this.isInState('delayed');
}
/**
* @returns true if the job is waiting for children.
*/
isWaitingChildren() {
return this.isInState('waiting-children');
}
/**
* @returns true if the job is active.
*/
isActive() {
return this.isInState('active');
}
/**
* @returns true if the job is waiting.
*/
async isWaiting() {
return this.isInState('waiting');
}
/**
* @returns the queue name this job belongs to.
*/
get queueName() {
return this.queue.name;
}
/**
* @returns the prefix that is used.
*/
get prefix() {
// The key `prefix` is a Redis concept; derive it from the qualified name
// (`"<prefix>:<queue>"`) rather than from queue options. Backends without a
// prefix (where the qualified name is just the queue name) yield `''`.
const qualified = this.queueQualifiedName;
const name = this.queueName;
return qualified.length > name.length + 1
? qualified.slice(0, qualified.length - name.length - 1)
: '';
}
/**
* Get current state.
*
* @returns Returns one of these values:
* 'completed', 'failed', 'delayed', 'active', 'waiting', 'waiting-children', 'unknown'.
*/
getState() {
return this.backend.getState(this.id);
}
/**
* Change delay of a delayed job.
*
* Reschedules a delayed job by setting a new delay from the current time.
* For example, calling changeDelay(5000) will reschedule the job to execute
* 5000 milliseconds (5 seconds) from now, regardless of the original delay.
*
* @param delay - milliseconds from now when the job should be processed.
* @returns void
* @throws JobNotExist
* This exception is thrown if jobId is missing.
* @throws JobNotInState
* This exception is thrown if job is not in delayed state.
*/
async changeDelay(delay) {
await this.backend.changeDelay(this.id, delay);
this.delay = delay;
}
/**
* Change job priority.
*
* @param opts - options containing priority and lifo values.
* @returns void
*/
async changePriority(opts) {
await this.backend.changePriority(this.id, opts.priority, opts.lifo);
this.priority = opts.priority || 0;
}
/**
* Get this jobs children result values if any.
*
* @returns Object mapping children job keys with their values.
*/
async getChildrenValues() {
const result = await this.backend.getProcessedChildrenValues(this.id);
if (result) {
return (0, utils_1.parseObjectValues)(result);
}
}
/**
* Retrieves the failures of child jobs that were explicitly ignored while using ignoreDependencyOnFailure option.
* This method is useful for inspecting which child jobs were intentionally ignored when an error occurred.
* @see {@link https://docs.bullmq.io/guide/flows/ignore-dependency}
*
* @returns Object mapping children job keys with their failure values.
*/
async getIgnoredChildrenFailures() {
return this.backend.getIgnoredChildrenFailures(this.id);
}
/**
* Get job's children failure values that were ignored if any.
*
* @deprecated This method is deprecated and will be removed in v6. Use getIgnoredChildrenFailures instead.
*
* @returns Object mapping children job keys with their failure values.
*/
async getFailedChildrenValues() {
return this.backend.getIgnoredChildrenFailures(this.id);
}
/**
* Get children job keys if this job is a parent and has children.
* @remarks
* Count options before Redis v7.2 works as expected with any quantity of entries
* on processed/unprocessed dependencies, since v7.2 you must consider that count
* won't have any effect until processed/unprocessed dependencies have a length
* greater than 127
* @see {@link https://redis.io/docs/management/optimization/memory-optimization/#redis--72}
* @see {@link https://docs.bullmq.io/guide/flows#getters}
* @returns dependencies separated by processed, unprocessed, ignored and failed.
*/
async getDependencies(opts = {}) {
return this.backend.getDependencies(this.id, opts);
}
/**
* Get children job counts if this job is a parent and has children.
*
* @returns dependencies count separated by processed, unprocessed, ignored and failed.
*/
async getDependenciesCount(opts = {}) {
const types = [];
Object.entries(opts).forEach(([key, value]) => {
if (value) {
types.push(key);
}
});
const finalTypes = types.length
? types
: ['processed', 'unprocessed', 'ignored', 'failed'];
const responses = await this.backend.getDependencyCounts(this.id, finalTypes);
const counts = {};
responses.forEach((res, index) => {
counts[`${finalTypes[index]}`] = res || 0;
});
return counts;
}
/**
* Returns a promise the resolves when the job has completed (containing the return value of the job),
* or rejects when the job has failed (containing the failedReason).
*
* @param queueEvents - Instance of QueueEvents.
* @param ttl - Time in milliseconds to wait for job to finish before timing out.
*/
async waitUntilFinished(queueEvents, ttl) {
await this.queue.waitUntilReady();
const jobId = this.id;
return new Promise(async (resolve, reject) => {
let timeout;
if (ttl) {
timeout = setTimeout(() => onFailed(
/* eslint-disable max-len */
`Job wait ${this.name} timed out before finishing, no finish notification arrived after ${ttl}ms (id=${jobId})`), ttl);
}
function onCompleted(args) {
removeListeners();
resolve(args.returnvalue);
}
function onFailed(args) {
removeListeners();
reject(new Error(args.failedReason || args));
}
const completedEvent = `completed:${jobId}`;
const failedEvent = `failed:${jobId}`;
queueEvents.on(completedEvent, onCompleted);
queueEvents.on(failedEvent, onFailed);
this.queue.on('closing', onFailed);
const removeListeners = () => {
clearInterval(timeout);
queueEvents.removeListener(completedEvent, onCompleted);
queueEvents.removeListener(failedEvent, onFailed);
this.queue.removeListener('closing', onFailed);
};
// Poll once right now to see if the job has already finished. The job may have been completed before we were able
// to register the event handlers on the QueueEvents, so we check here to make sure we're not waiting for an event
// that has already happened. We block checking the job until the queue events object is actually listening to
// Redis so there's no chance that it will miss events.
await queueEvents.waitUntilReady();
const [status, result] = (await this.backend.isFinished(jobId, true));
const finished = status != 0;
if (finished) {
if (status == -1 || status == 2) {
onFailed({ failedReason: result });
}
else {
onCompleted({ returnvalue: getReturnValue(result) });
}
}
});
}
/**
* Moves the job to the delay set.
*
* @param timestamp - timestamp when the job should be moved back to "wait"
* @param token - token to check job is locked by current worker
* @returns
*/
async moveToDelayed(timestamp, token) {
const now = Date.now();
const delay = timestamp - now;
const finalDelay = delay > 0 ? delay : 0;
await this.backend.moveToDelayed(this.id, now, finalDelay, token, {
skipAttempt: true,
});
this.delay = finalDelay;
this.recordJobMetrics('delayed');
}
/**
* Moves the job to the waiting-children set.
*
* @param token - Token to check job is locked by current worker
* @param opts - The options bag for moving a job to waiting-children.
* @returns true if the job was moved
*/
async moveToWaitingChildren(token, opts = {}) {
const movedToWaitingChildren = await this.backend.moveToWaitingChildren(this.id, token, opts);
if (movedToWaitingChildren) {
this.recordJobMetrics('waiting-children');
}
return movedToWaitingChildren;
}
/**
* Promotes a delayed job so that it starts to be processed as soon as possible.
*/
async promote() {
const jobId = this.id;
await this.backend.promote(jobId);
this.delay = 0;
}
/**
* Attempts to retry the job. Only a job that has failed or completed can be retried.
*
* @param state - completed / failed
* @param opts - options to retry a job
* @returns A promise that resolves when the job has been successfully moved to the wait queue.
* The queue emits a waiting event when the job is successfully moved.
* @throws Will throw an error if the job does not exist, is locked, or is not in the expected state.
*/
async retry(state = 'failed', opts = {}) {
await this.backend.retryFinishedJob(this, state, opts);
this.failedReason = null;
this.finishedOn = null;
this.processedOn = null;
this.returnvalue = null;
if (opts.resetAttemptsMade) {
this.attemptsMade = 0;
}
if (opts.resetAttemptsStarted) {
this.attemptsStarted = 0;
}
}
async isInState(state) {
return this.backend.isJobInState(state, this.id);
}
/**
* Adds the job to Redis.
*
* @param client - The Redis client to use for adding the job.
* @param parentOpts - Options for the parent-child relationship.
* @returns The job ID
*/
addJob(client, parentOpts) {
const jobData = this.asJSON();
this.validateOptions(jobData);
// `addJobToTransaction` operates on a raw Redis client/transaction and is
// therefore specific to the Redis backend implementation.
return this.backend.addJobToTransaction(client, jobData, this.id, parentOpts);
}
/**
* Builds the data needed to insert this job as part of a flow, without
* touching the datastore. Used by FlowProducer to collect a whole flow tree
* and hand it to the backend's atomic `addFlow` operation.
*
* @param parentOpts - parent-link options for this node.
*/
toFlowEntry(parentOpts = {}) {
const jobData = this.asJSON();
this.validateOptions(jobData);
return {
jobData,
jobId: this.id,
parentKeyOpts: parentOpts,
prefix: this.prefix,
queueName: this.queueName,
};
}
/**
* Removes a deduplication key if job is still the cause of deduplication.
* @returns true if the deduplication key was removed.
*/
async removeDeduplicationKey() {
if (this.deduplicationId) {
const result = await this.backend.removeDeduplicationKey(this.deduplicationId, this.id);
return result > 0;
}
return false;
}
validateOptions(jobData) {
var _a, _b, _c, _d, _e, _f;
const exclusiveOptions = [
'removeDependencyOnFailure',
'failParentOnFailure',
'continueParentOnFailure',
'ignoreDependencyOnFailure',
];
const exceedLimit = this.opts.sizeLimit &&
(0, utils_1.lengthInUtf8Bytes)(jobData.data) > this.opts.sizeLimit;
if (exceedLimit) {
throw new Error(`The size of job ${this.name} exceeds the limit ${this.opts.sizeLimit} bytes`);
}
const enabledExclusiveOptions = exclusiveOptions.filter(opt => this.opts[opt]);
if (enabledExclusiveOptions.length > 1) {
const optionsList = enabledExclusiveOptions.join(', ');
throw new Error(`The following options cannot be used together: ${optionsList}`);
}
if ((_a = this.opts) === null || _a === void 0 ? void 0 : _a.jobId) {
if (`${parseInt(this.opts.jobId, 10)}` === ((_b = this.opts) === null || _b === void 0 ? void 0 : _b.jobId)) {
throw new Error('Custom Id cannot be integers');
}
// TODO: replace this check in next breaking check with include(':')
// By using split we are still keeping compatibility with old repeatable jobs
if (((_c = this.opts) === null || _c === void 0 ? void 0 : _c.jobId.includes(':')) &&
((_e = (_d = this.opts) === null || _d === void 0 ? void 0 : _d.jobId) === null || _e === void 0 ? void 0 : _e.split(':').length) !== 3) {
throw new Error('Custom Id cannot contain :');
}
}
if (this.opts.priority) {
if (Math.trunc(this.opts.priority) !== this.opts.priority) {
throw new Error(`Priority should not be float`);
}
if (this.opts.priority > exports.PRIORITY_LIMIT) {
throw new Error(`Priority should be between 0 and ${exports.PRIORITY_LIMIT}`);
}
}
if (this.opts.deduplication) {
if (!((_f = this.opts.deduplication) === null || _f === void 0 ? void 0 : _f.id)) {
throw new Error('Deduplication id must be provided');
}
if (this.parentKey) {
throw new Error('Deduplication and parent options cannot be used together');
}
}
if (Object.prototype.hasOwnProperty.call(this.opts, 'debounce')) {
throw new Error('Debounce option has been removed. Use deduplication option instead');
}
if (typeof this.opts.backoff === 'object' &&
typeof this.opts.backoff.jitter === 'number') {
if (this.opts.backoff.jitter < 0 || this.opts.backoff.jitter > 1) {
throw new Error(`Jitter should be between 0 and 1`);
}
}
}
updateStacktrace(err) {
this.stacktrace = this.stacktrace || [];
if (err === null || err === void 0 ? void 0 : err.stack) {
this.stacktrace.push(err.stack);
if (this.opts.stackTraceLimit === 0) {
this.stacktrace = [];
}
else if (this.opts.stackTraceLimit) {
this.stacktrace = this.stacktrace.slice(-this.opts.stackTraceLimit);
}
}
}
setSpanJobAttributes(span) {
span === null || span === void 0 ? void 0 : span.setAttributes({
[enums_1.TelemetryAttributes.JobName]: this.name,
[enums_1.TelemetryAttributes.JobId]: this.id,
});
}
}
exports.Job = Job;
function getTraces(stacktrace) {
if (!stacktrace) {
return [];
}
const traces = (0, utils_1.tryCatch)(JSON.parse, JSON, [stacktrace]);
if (traces === utils_1.errorObject || !(traces instanceof Array)) {
return [];
}
else {
return traces;
}
}
function getReturnValue(_value) {
const value = (0, utils_1.tryCatch)(JSON.parse, JSON, [_value]);
if (value !== utils_1.errorObject) {
return value;
}
else {
logger('corrupted returnvalue: ' + _value, value);
}
}

164
node_modules/bullmq/dist/cjs/classes/lock-manager.js generated vendored Normal file
View File

@@ -0,0 +1,164 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.LockManager = void 0;
const abort_controller_1 = require("./abort-controller");
const enums_1 = require("../enums");
/**
* Manages lock renewal for BullMQ workers.
* It periodically extends locks for active jobs to prevent them from being
* considered stalled by other workers.
*/
class LockManager {
constructor(worker, opts) {
this.worker = worker;
this.opts = opts;
// Maps job ids with their tokens, timestamps, and abort controllers
this.trackedJobs = new Map();
this.closed = false;
}
/**
* Starts the lock manager timers for lock renewal.
*/
start() {
if (this.closed) {
return;
}
// Start lock renewal timer if not disabled
if (this.opts.lockRenewTime > 0) {
this.startLockExtenderTimer();
}
}
async extendLocks(jobIds) {
await this.worker.trace(enums_1.SpanKind.INTERNAL, 'extendLocks', this.worker.name, async (span) => {
span === null || span === void 0 ? void 0 : span.setAttributes({
[enums_1.TelemetryAttributes.WorkerId]: this.opts.workerId,
[enums_1.TelemetryAttributes.WorkerName]: this.opts.workerName,
[enums_1.TelemetryAttributes.WorkerJobsToExtendLocks]: jobIds,
});
try {
const jobTokens = jobIds.map(id => { var _a; return ((_a = this.trackedJobs.get(id)) === null || _a === void 0 ? void 0 : _a.token) || ''; });
const erroredJobIds = await this.worker.extendJobLocks(jobIds, jobTokens, this.opts.lockDuration);
if (erroredJobIds.length > 0) {
this.worker.emit('lockRenewalFailed', erroredJobIds);
for (const jobId of erroredJobIds) {
this.worker.emit('error', new Error(`could not renew lock for job ${jobId}`));
}
}
const succeededJobIds = jobIds.filter(id => !erroredJobIds.includes(id));
if (succeededJobIds.length > 0) {
this.worker.emit('locksRenewed', {
count: succeededJobIds.length,
jobIds: succeededJobIds,
});
}
}
catch (err) {
this.worker.emit('error', err);
}
});
}
startLockExtenderTimer() {
clearTimeout(this.lockRenewalTimer);
if (!this.closed) {
this.lockRenewalTimer = setTimeout(async () => {
// Get all the jobs whose locks expire in less than 1/2 of the lockRenewTime
const now = Date.now();
const jobsToExtend = [];
for (const jobId of this.trackedJobs.keys()) {
const tracked = this.trackedJobs.get(jobId);
const { ts, token, abortController } = tracked;
if (!ts) {
this.trackedJobs.set(jobId, { token, ts: now, abortController });
continue;
}
if (ts + this.opts.lockRenewTime / 2 < now) {
this.trackedJobs.set(jobId, { token, ts: now, abortController });
jobsToExtend.push(jobId);
}
}
if (jobsToExtend.length) {
await this.extendLocks(jobsToExtend);
}
this.startLockExtenderTimer();
}, this.opts.lockRenewTime / 2);
}
}
/**
* Stops the lock manager and clears all timers.
*/
async close() {
if (this.closed) {
return;
}
this.closed = true;
if (this.lockRenewalTimer) {
clearTimeout(this.lockRenewalTimer);
this.lockRenewalTimer = undefined;
}
this.trackedJobs.clear();
}
/**
* Adds a job to be tracked for lock renewal.
* Returns an AbortController if shouldCreateController is true, undefined otherwise.
*/
trackJob(jobId, token, ts, shouldCreateController = false) {
const abortController = shouldCreateController
? new abort_controller_1.AbortController()
: undefined;
if (!this.closed && jobId) {
this.trackedJobs.set(jobId, { token, ts, abortController });
}
return abortController;
}
/**
* Removes a job from lock renewal tracking.
*/
untrackJob(jobId) {
this.trackedJobs.delete(jobId);
}
/**
* Gets the number of jobs currently being tracked.
*/
getActiveJobCount() {
return this.trackedJobs.size;
}
/**
* Checks if the lock manager is running.
*/
isRunning() {
return !this.closed && this.lockRenewalTimer !== undefined;
}
/**
* Cancels a specific job by aborting its signal.
* @param jobId - The ID of the job to cancel
* @param reason - Optional reason for the cancellation
* @returns true if the job was found and cancelled, false otherwise
*/
cancelJob(jobId, reason) {
const tracked = this.trackedJobs.get(jobId);
if (tracked === null || tracked === void 0 ? void 0 : tracked.abortController) {
tracked.abortController.abort(reason);
return true;
}
return false;
}
/**
* Cancels all tracked jobs by aborting their signals.
* @param reason - Optional reason for the cancellation
*/
cancelAllJobs(reason) {
for (const tracked of this.trackedJobs.values()) {
if (tracked.abortController) {
tracked.abortController.abort(reason);
}
}
}
/**
* Gets a list of all tracked job IDs.
* @returns Array of job IDs currently being tracked
*/
getTrackedJobIds() {
return Array.from(this.trackedJobs.keys());
}
}
exports.LockManager = LockManager;

46
node_modules/bullmq/dist/cjs/classes/main-base.js generated vendored Normal file
View File

@@ -0,0 +1,46 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
/**
* Wrapper for sandboxing.
*
*/
const child_processor_1 = require("./child-processor");
const enums_1 = require("../enums");
const utils_1 = require("../utils");
exports.default = (send, receiver) => {
const childProcessor = new child_processor_1.ChildProcessor(send, receiver);
receiver === null || receiver === void 0 ? void 0 : receiver.on('message', async (msg) => {
try {
switch (msg.cmd) {
case enums_1.ChildCommand.Init:
await childProcessor.init(msg.value);
break;
case enums_1.ChildCommand.Start:
await childProcessor.start(msg.job, msg === null || msg === void 0 ? void 0 : msg.token);
break;
case enums_1.ChildCommand.Stop:
break;
case enums_1.ChildCommand.Cancel:
childProcessor.cancel(msg.value);
break;
}
}
catch (err) {
console.error('Error handling child message');
}
});
process.on('SIGTERM', () => childProcessor.waitForCurrentJobAndExit());
process.on('SIGINT', () => childProcessor.waitForCurrentJobAndExit());
process.on('uncaughtException', async (err) => {
if (typeof err !== 'object') {
err = new Error((0, utils_1.toString)(err));
}
await send({
cmd: enums_1.ParentCommand.Failed,
value: (0, utils_1.errorToJSON)(err),
});
// An uncaughException leaves this process in a potentially undetermined state so
// we must exit
process.exit();
});
};

View File

@@ -0,0 +1 @@
export {};

9
node_modules/bullmq/dist/cjs/classes/main-worker.js generated vendored Normal file
View File

@@ -0,0 +1,9 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
/**
* Worker Thread wrapper for sandboxing
*
*/
const worker_threads_1 = require("worker_threads");
const main_base_1 = require("./main-base");
(0, main_base_1.default)(async (msg) => worker_threads_1.parentPort.postMessage(msg), worker_threads_1.parentPort);

1
node_modules/bullmq/dist/cjs/classes/main.d.ts generated vendored Normal file
View File

@@ -0,0 +1 @@
export {};

9
node_modules/bullmq/dist/cjs/classes/main.js generated vendored Normal file
View File

@@ -0,0 +1,9 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
/**
* Child process wrapper for sandboxing.
*
*/
const utils_1 = require("../utils");
const main_base_1 = require("./main-base");
(0, main_base_1.default)((msg) => (0, utils_1.childSend)(process, msg), process);

View File

@@ -0,0 +1,771 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createNodeRedisClient = createNodeRedisClient;
const tslib_1 = require("tslib");
const crypto_1 = require("crypto");
const events_1 = require("events");
const stream_1 = require("stream");
const connection_closed_error_1 = require("./errors/connection-closed-error");
function normalizeScriptArgs(args) {
return args.length === 1 && Array.isArray(args[0]) ? args[0] : args;
}
function isConnectionClosedError(err) {
return ((err === null || err === void 0 ? void 0 : err.message) === 'Disconnects client' ||
(err === null || err === void 0 ? void 0 : err.message) === 'The client is closed' ||
(err === null || err === void 0 ? void 0 : err.message) === 'Connection is closed.');
}
function createNodeRedisClient(client) {
return new NodeRedisAdapter(client);
}
/**
* Full wrapper (not augmentation) because node-redis's API is structurally
* different from ioredis and cannot be patched in-place.
*/
class NodeRedisAdapter extends events_1.EventEmitter {
/**
* Expose connection status using the vocabulary that
* {@link RedisConnection.waitUntilReady} expects:
* 'wait' → not yet connected, call connect()
* 'ready' → usable
* 'end' → permanently closed
*/
get status() {
if (this.statusOverride) {
return this.statusOverride;
}
if (this.raw.isReady) {
return 'ready';
}
if (this.raw.isOpen) {
return 'connect';
}
// Distinguish "never connected" from "disconnected after use"
return this.hasConnected ? 'end' : 'wait';
}
set status(val) {
// Allow RedisConnection to forcibly set 'end'
if (val === 'end') {
this.destroying = true;
if (this.raw.isOpen) {
try {
this.raw.quit().catch(() => { });
}
catch (_a) {
// already closed
}
}
}
this.statusOverride = val;
}
get options() {
var _a;
return (_a = this.raw.options) !== null && _a !== void 0 ? _a : {};
}
set options(val) {
// no-op – callers sometimes assign
}
constructor(raw) {
super();
this.raw = raw;
this.scripts = new Map();
this.hasConnected = false;
this.destroying = false;
this.isCluster = false; // TODO: cluster support
// Track first connection so status can distinguish 'wait' vs 'end'.
// When a connectionName is set (via duplicate()), delay the 'ready'
// event until CLIENT SETNAME completes so that callers waiting for
// 'ready' (e.g. RedisConnection.waitUntilReady) see the name already
// applied.
raw.on('ready', () => {
this.hasConnected = true;
if (this.connectionName) {
this.raw.clientSetName(this.connectionName).then(() => this.emit('ready'), () => this.emit('ready'));
}
else {
this.emit('ready');
}
});
raw.on('error', (err) => {
// Suppress the expected DisconnectsClientError that node-redis emits
// when destroy() is called intentionally (e.g. during close/disconnect).
if (this.destroying && isConnectionClosedError(err)) {
return;
}
this.emit('error', err);
});
raw.on('end', () => this.emit('close'));
raw.on('reconnecting', () => this.emit('reconnecting'));
// Auto-connect eagerly, like ioredis does in its constructor.
// This ensures commands can be issued immediately without an
// explicit connect() call. The promise is stored so that
// connect() is idempotent and callers can still await it.
if (!raw.isOpen) {
this.connectPromise = raw.connect().then(() => {
this.connectPromise = undefined;
}, (err) => {
this.connectPromise = undefined;
// Don't throw — errors surface via the 'error' event.
});
}
}
// ---------------------------------------------------------------
// Connection lifecycle
// ---------------------------------------------------------------
async connect() {
if (this.connectPromise) {
return this.connectPromise;
}
if (!this.raw.isOpen) {
this.connectPromise = this.raw.connect().then(() => {
this.connectPromise = undefined;
}, (err) => {
this.connectPromise = undefined;
throw err;
});
return this.connectPromise;
}
if (!this.raw.isReady) {
await new Promise((resolve, reject) => {
const onReady = () => {
cleanup();
resolve();
};
const onError = (err) => {
cleanup();
reject(err);
};
const onEnd = () => {
cleanup();
reject(new Error('Connection ended before ready event'));
};
const cleanup = () => {
this.off('ready', onReady);
this.off('error', onError);
this.off('end', onEnd);
};
this.once('ready', onReady);
this.once('error', onError);
this.once('end', onEnd);
});
}
}
disconnect(reconnect = false) {
this.destroying = true;
if (!reconnect) {
this.statusOverride = 'end';
}
try {
if (this.raw.isOpen) {
// Use destroy() for immediate teardown. This interrupts any pending
// blocking commands (e.g. BZPOPMIN) without waiting for them to
// complete. The resulting "Disconnects client" rejections are handled
// by BullMQ's isNotConnectionError() checks.
this.raw.destroy();
}
}
catch (_a) {
// Swallow errors from already-closed connections
}
this.emit('close');
if (reconnect) {
this.statusOverride = undefined;
this.emit('reconnecting');
this.connect()
.catch(err => {
if (!isConnectionClosedError(err)) {
this.emit('error', err);
}
})
.finally(() => {
this.destroying = false;
});
}
else {
// Emit both 'close' and 'end' so that all listeners are unblocked.
// RedisConnection.close() listens for 'close', RedisConnection.disconnect() listens for 'end'.
this.emit('end');
}
}
async quit() {
if (this.destroying || this.statusOverride === 'end') {
setImmediate(() => {
this.emit('end');
this.emit('close');
});
return 'OK';
}
this.destroying = true;
try {
if (this.raw.isOpen) {
try {
await this.raw.quit();
}
catch (_a) {
// Swallow errors from already-closing connections
}
}
}
catch (_b) {
// Swallow errors from already-closing connections
}
this.statusOverride = 'end';
// Emit on next tick so callers can register listeners after await quit()
setImmediate(() => {
this.emit('end');
this.emit('close');
});
return 'OK';
}
duplicate(...args) {
const dup = this.raw.duplicate();
const adapter = new NodeRedisAdapter(dup);
// Copy registered scripts to the duplicate
for (const [name, script] of this.scripts) {
adapter.scripts.set(name, script);
adapter[name] = (...args) => adapter.runCommand(name, args);
}
// Handle connectionName option (ioredis calls CLIENT SETNAME automatically).
// Setting connectionName BEFORE auto-connect resolves ensures the
// constructor's 'ready' handler applies CLIENT SETNAME before emitting
// 'ready', so callers (like RedisConnection.waitUntilReady) see the name
// already set when the connection is reported as ready.
const opts = args[0];
if (opts && typeof opts === 'object' && opts.connectionName) {
adapter.connectionName = opts.connectionName;
}
return adapter;
}
// ---------------------------------------------------------------
// Lua script engine
// ---------------------------------------------------------------
defineCommand(name, definition) {
const sha = (0, crypto_1.createHash)('sha1').update(definition.lua).digest('hex');
this.scripts.set(name, {
sha,
lua: definition.lua,
numberOfKeys: definition.numberOfKeys,
});
// Mimic ioredis behavior: add a callable property on the instance
// so that `(client as any)[name]` is defined (used by ScriptLoader cache check)
this[name] = (...args) => this.runCommand(name, args);
// Pre-load the script into Redis so that EVALSHA in transactions works
// immediately. This mirrors what ioredis does under the hood.
this.raw.scriptLoad(definition.lua).catch(() => {
// Ignore errors here – runCommand has NOSCRIPT fallback for non-tx path
});
}
async runCommand(name, args) {
var _a, _b;
const script = this.scripts.get(name);
if (!script) {
throw new Error(`BullMQ: unknown command "${name}"`);
}
const commandArgs = normalizeScriptArgs(args);
const { sha, lua, numberOfKeys } = script;
const keys = commandArgs.slice(0, numberOfKeys).map(String);
// Preserve Buffer arguments (e.g. msgpack data) – only stringify non-Buffers.
// Convert undefined/null to '' to match ioredis behavior (keeps arg positions).
const argv = commandArgs.slice(numberOfKeys).map((a) => {
if (Buffer.isBuffer(a)) {
return a;
}
if (a === undefined || a === null) {
return '';
}
return String(a);
});
try {
return await this.raw.evalSha(sha, { keys, arguments: argv });
}
catch (err) {
if (this.destroying && isConnectionClosedError(err)) {
return null;
}
if (isConnectionClosedError(err)) {
throw new connection_closed_error_1.ConnectionClosedError(err.message, err);
}
// NOSCRIPT – fall back to EVAL which also caches the script
if ((_b = (_a = err === null || err === void 0 ? void 0 : err.message) === null || _a === void 0 ? void 0 : _a.includes) === null || _b === void 0 ? void 0 : _b.call(_a, 'NOSCRIPT')) {
try {
return await this.raw.eval(lua, { keys, arguments: argv });
}
catch (evalErr) {
if (this.destroying && isConnectionClosedError(evalErr)) {
return null;
}
if (isConnectionClosedError(evalErr)) {
throw new connection_closed_error_1.ConnectionClosedError(evalErr.message, evalErr);
}
throw evalErr;
}
}
throw err;
}
}
// ---------------------------------------------------------------
// Pipeline / Transaction
// ---------------------------------------------------------------
multi() {
return new NodeRedisTransaction(this.raw.multi(), this.scripts);
}
pipeline() {
// node-redis doesn't have a separate pipeline concept;
// use multi() which behaves similarly for batching.
return this.multi();
}
// ---------------------------------------------------------------
// Hash commands
// ---------------------------------------------------------------
async hgetall(key) {
const result = await this.raw.hGetAll(key);
return result !== null && result !== void 0 ? result : {};
}
async hget(key, field) {
var _a;
return (_a = (await this.raw.hGet(key, field))) !== null && _a !== void 0 ? _a : null;
}
async hmget(key, ...fields) {
const result = await this.raw.hmGet(key, fields);
return result.map((v) => v !== null && v !== void 0 ? v : null);
}
async hset(key, dataOrField, ...rest) {
if (typeof dataOrField === 'object') {
// Record-based call: hset(key, { field: value, ... })
return await this.raw.hSet(key, dataOrField);
}
// Varargs call (ioredis compat): hset(key, field, value, field, value, ...)
const record = {};
record[dataOrField] = String(rest[0]);
for (let i = 1; i < rest.length; i += 2) {
record[String(rest[i])] = String(rest[i + 1]);
}
return await this.raw.hSet(key, record);
}
async hdel(key, ...fields) {
return await this.raw.hDel(key, fields);
}
async hexists(key, field) {
const exists = await this.raw.hExists(key, field);
return exists ? 1 : 0;
}
// ---------------------------------------------------------------
// String commands
// ---------------------------------------------------------------
async get(key) {
var _a;
return (_a = (await this.raw.get(key))) !== null && _a !== void 0 ? _a : null;
}
async set(key, value, options) {
const opts = {};
if ((options === null || options === void 0 ? void 0 : options.PX) != null) {
opts.PX = options.PX;
}
else if ((options === null || options === void 0 ? void 0 : options.EX) != null) {
opts.EX = options.EX;
}
return await this.raw.set(key, String(value), opts);
}
async del(...keys) {
if (keys.length === 0) {
return 0;
}
return await this.raw.del(keys);
}
// ---------------------------------------------------------------
// Sorted set commands
// ---------------------------------------------------------------
async zrange(key, start, end, options) {
if (options === null || options === void 0 ? void 0 : options.WITHSCORES) {
// node-redis v5 uses a separate method for WITHSCORES
const items = await this.raw.zRangeWithScores(key, start, end);
// Flatten to [member, score, member, score, …] like ioredis
const flat = [];
for (const item of items) {
flat.push(item.value, String(item.score));
}
return flat;
}
return await this.raw.zRange(key, start, end);
}
async zrevrange(key, start, end, options) {
if (options === null || options === void 0 ? void 0 : options.WITHSCORES) {
const items = await this.raw.zRangeWithScores(key, start, end, {
REV: true,
});
const flat = [];
for (const item of items) {
flat.push(item.value, String(item.score));
}
return flat;
}
return await this.raw.zRange(key, start, end, { REV: true });
}
async zcard(key) {
return await this.raw.zCard(key);
}
async zscore(key, member) {
const score = await this.raw.zScore(key, member);
return score != null ? String(score) : null;
}
// ---------------------------------------------------------------
// List commands
// ---------------------------------------------------------------
async lrange(key, start, end) {
return await this.raw.lRange(key, start, end);
}
async llen(key) {
return await this.raw.lLen(key);
}
async ltrim(key, start, end) {
await this.raw.lTrim(key, start, end);
return 'OK';
}
async lpos(key, value) {
var _a;
return (_a = (await this.raw.lPos(key, value))) !== null && _a !== void 0 ? _a : null;
}
// ---------------------------------------------------------------
// Set commands
// ---------------------------------------------------------------
async smembers(key) {
return await this.raw.sMembers(key);
}
// ---------------------------------------------------------------
// Stream commands
// ---------------------------------------------------------------
async xadd(key, id, fields, options) {
const opts = {};
if ((options === null || options === void 0 ? void 0 : options.MAXLEN) != null) {
opts.TRIM = {
strategy: 'MAXLEN',
threshold: options.MAXLEN,
strategyModifier: options.approximate === false ? undefined : '~',
};
}
// node-redis xAdd rejects numeric field values — stringify all values
const strFields = {};
for (const [k, v] of Object.entries(fields)) {
strFields[k] = String(v);
}
return await this.raw.xAdd(key, id, strFields, opts);
}
async xread(streams, options) {
const opts = {};
if ((options === null || options === void 0 ? void 0 : options.BLOCK) != null) {
opts.BLOCK = options.BLOCK;
}
if ((options === null || options === void 0 ? void 0 : options.COUNT) != null) {
opts.COUNT = options.COUNT;
}
const streamArgs = streams.map(s => ({ key: s.key, id: s.id }));
let result;
try {
result = await this.raw.xRead(streamArgs, opts);
}
catch (err) {
if (this.destroying && isConnectionClosedError(err)) {
return null;
}
if (isConnectionClosedError(err)) {
throw new connection_closed_error_1.ConnectionClosedError(err.message, err);
}
throw err;
}
if (!result) {
return null;
}
// Normalize to ioredis format: [[streamName, [[id, [field, value, …]], …]], …]
return result.map((stream) => [
stream.name,
stream.messages.map((msg) => [
msg.id,
Object.entries(msg.message).flat(),
]),
]);
}
async xtrim(key, strategy, threshold, options) {
const strategyModifier = (options === null || options === void 0 ? void 0 : options.approximate) === false ? undefined : '~';
return await this.raw.xTrim(key, strategy, threshold, {
strategyModifier,
});
}
// ---------------------------------------------------------------
// Blocking commands
// ---------------------------------------------------------------
async bzpopmin(key, timeout) {
let result;
try {
result = await this.raw.bzPopMin(key, timeout);
}
catch (err) {
if (this.destroying && isConnectionClosedError(err)) {
return null;
}
if (isConnectionClosedError(err)) {
throw new connection_closed_error_1.ConnectionClosedError(err.message, err);
}
throw err;
}
if (!result) {
return null;
}
return [result.key, result.value, String(result.score)];
}
// ---------------------------------------------------------------
// Server / admin commands
// ---------------------------------------------------------------
async info() {
return await this.raw.info();
}
async clientSetName(name) {
return await this.raw.clientSetName(name);
}
async clientList() {
return await this.raw.sendCommand(['CLIENT', 'LIST']);
}
// ---------------------------------------------------------------
// Key scanning
// ---------------------------------------------------------------
async scan(cursor, options) {
const opts = {};
if (options === null || options === void 0 ? void 0 : options.MATCH) {
opts.MATCH = options.MATCH;
}
if (options === null || options === void 0 ? void 0 : options.COUNT) {
opts.COUNT = options.COUNT;
}
const result = await this.raw.scan(String(cursor), opts);
return [String(result.cursor), result.keys];
}
scanStream(options) {
const raw = this.raw;
const connectPromise = this.connectPromise;
const scanOpts = {};
if (options.match) {
scanOpts.MATCH = options.match;
}
if (options.count) {
scanOpts.COUNT = options.count;
}
const readable = new stream_1.Readable({
objectMode: true,
async read() {
var _a, e_1, _b, _c;
try {
if (connectPromise) {
await connectPromise;
}
try {
for (var _d = true, _e = tslib_1.__asyncValues(raw.scanIterator(scanOpts)), _f; _f = await _e.next(), _a = _f.done, !_a; _d = true) {
_c = _f.value;
_d = false;
const keys = _c;
if (!readable.push(Array.isArray(keys) ? keys : [keys])) {
return; // backpressure
}
}
}
catch (e_1_1) { e_1 = { error: e_1_1 }; }
finally {
try {
if (!_d && !_a && (_b = _e.return)) await _b.call(_e);
}
finally { if (e_1) throw e_1.error; }
}
readable.push(null); // EOF
}
catch (err) {
readable.destroy(err);
}
},
});
return readable;
}
// ---------------------------------------------------------------
// Extra Redis commands (not part of IRedisClient but used by tests
// and occasionally by user code that accesses the raw client).
// ---------------------------------------------------------------
async keys(pattern) {
return await this.raw.keys(pattern);
}
async exists(...keys) {
if (keys.length === 0) {
return 0;
}
return await this.raw.exists(keys);
}
async zadd(key, ...args) {
// ioredis: zadd(key, score, member, score, member, ...)
const members = [];
for (let i = 0; i < args.length; i += 2) {
members.push({ score: Number(args[i]), value: String(args[i + 1]) });
}
return await this.raw.zAdd(key, members);
}
async zrem(key, ...members) {
return await this.raw.zRem(key, members);
}
async xlen(key) {
return await this.raw.xLen(key);
}
async xrevrange(key, end, start, ...rest) {
const opts = {};
// ioredis: xrevrange(key, end, start, 'COUNT', n)
if (rest[0] === 'COUNT') {
opts.COUNT = Number(rest[1]);
}
const result = await this.raw.xRevRange(key, end, start, opts);
// Normalize to ioredis format: [[id, [field, value, …]], …]
return result.map((msg) => [
msg.id,
Object.entries(msg.message).flat(),
]);
}
async sadd(key, ...members) {
return await this.raw.sAdd(key, members.map(String));
}
async scard(key) {
return await this.raw.sCard(key);
}
async lpush(key, ...values) {
return await this.raw.lPush(key, values);
}
async rpop(key) {
return await this.raw.rPop(key);
}
async incr(key) {
return await this.raw.incr(key);
}
async incrby(key, increment) {
return await this.raw.incrBy(key, increment);
}
async flushall() {
return await this.raw.flushAll();
}
}
// ---------------------------------------------------------------------------
// Transaction / Pipeline wrapper
// ---------------------------------------------------------------------------
class NodeRedisTransaction {
constructor(raw, scripts) {
this.raw = raw;
this.scripts = scripts;
this.transformers = [];
}
addIdentityTransformer() {
this.transformers.push((v) => v);
}
hgetall(key) {
this.raw.hGetAll(key);
this.addIdentityTransformer();
return this;
}
hset(key, data) {
this.raw.hSet(key, data);
this.addIdentityTransformer();
return this;
}
hscan(key, cursor, options) {
const opts = {};
if ((options === null || options === void 0 ? void 0 : options.COUNT) != null) {
opts.COUNT = options.COUNT;
}
this.raw.hScan(key, String(cursor), opts);
// Transform node-redis { cursor, entries: [{field, value}] }
// to ioredis [cursor, [field, value, field, value, ...]]
this.transformers.push((val) => {
if (!val) {
return ['0', []];
}
const flat = [];
for (const entry of val.entries || []) {
flat.push(entry.field, entry.value);
}
return [String(val.cursor), flat];
});
return this;
}
smembers(key) {
this.raw.sMembers(key);
this.addIdentityTransformer();
return this;
}
sscan(key, cursor, options) {
const opts = {};
if ((options === null || options === void 0 ? void 0 : options.COUNT) != null) {
opts.COUNT = options.COUNT;
}
this.raw.sScan(key, String(cursor), opts);
// Transform node-redis { cursor, members: [...] }
// to ioredis [cursor, [member, member, ...]]
this.transformers.push((val) => {
if (!val) {
return ['0', []];
}
return [String(val.cursor), val.members || []];
});
return this;
}
zrange(key, start, end) {
this.raw.zRange(key, start, end);
this.addIdentityTransformer();
return this;
}
lrange(key, start, end) {
this.raw.lRange(key, start, end);
this.addIdentityTransformer();
return this;
}
llen(key) {
this.raw.lLen(key);
this.addIdentityTransformer();
return this;
}
del(...keys) {
if (keys.length > 0) {
this.raw.del(keys);
this.addIdentityTransformer();
}
return this;
}
runCommand(name, args) {
const script = this.scripts.get(name);
if (!script) {
throw new Error(`BullMQ: unknown command "${name}" in transaction`);
}
const commandArgs = normalizeScriptArgs(args);
const { sha, lua, numberOfKeys } = script;
const cmdKeys = commandArgs.slice(0, numberOfKeys).map(String);
const argv = commandArgs.slice(numberOfKeys).map((a) => {
if (Buffer.isBuffer(a)) {
return a;
}
if (a === undefined || a === null) {
return '';
}
return String(a);
});
// Use evalSha in pipeline; NOSCRIPT handling happens at exec()-time
// node-redis multi supports evalSha
this.raw.evalSha(sha, { keys: cmdKeys, arguments: argv });
this.addIdentityTransformer();
return this;
}
async exec() {
const results = await this.raw.exec();
if (!results) {
return null;
}
// node-redis multi.exec() returns values directly (or throws on error).
// Normalize to ioredis format: [Error | null, value][]
// Apply per-command transformers for format differences (hscan, sscan).
return results.map((result, i) => {
if (result instanceof Error) {
return [result, null];
}
const transformer = this.transformers[i];
const value = transformer ? transformer(result) : result;
return [null, value];
});
}
}

156
node_modules/bullmq/dist/cjs/classes/queue-base.js generated vendored Normal file
View File

@@ -0,0 +1,156 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.QueueBase = void 0;
const events_1 = require("events");
const utils_1 = require("../utils");
const create_backend_1 = require("../utils/create-backend");
const job_1 = require("./job");
/**
* Base class for all classes that need to interact with queues.
* This class is normally not used directly, but extended by the other classes.
*
*/
class QueueBase extends events_1.EventEmitter {
/**
*
* @param name - The name of the queue.
* @param opts - Options for the queue.
* @param backendFactory - Factory used to build the {@link IQueueBackend}.
* Defaults to the Redis backend; inject a different factory to use another
* datastore or a test mock.
*/
constructor(name, opts = { connection: {} }, backendFactory = (0, create_backend_1.getDefaultBackendFactory)(), hasBlockingConnection = false) {
super();
this.name = name;
this.opts = opts;
this.closed = false;
this.hasBlockingConnection = false;
this.backendFactory = backendFactory;
this.hasBlockingConnection = hasBlockingConnection;
this.opts = Object.assign({}, opts);
if (!name) {
throw new Error('Queue name must be provided');
}
if (name.includes(':')) {
throw new Error('Queue name cannot contain :');
}
this.createBackend();
// Queue identity and key building are owned by the backend (a datastore
// concern). The Redis backend encodes the key `prefix`
// (`"<prefix>:<queue>"`); other backends format their own identity.
this.qualifiedName = this.backend.qualifiedName;
this.keys = this.backend.keys;
this.toKey = (type) => this.backend.toKey(type);
this.backend.on('error', (error) => this.emit('error', error));
this.backend.on('close', () => {
if (!this.closing) {
this.emit('ioredis:close');
}
});
}
/**
* Resolves once the underlying backend (and its connection) is ready.
*/
waitUntilReady() {
return this.backend.waitUntilReady();
}
/**
* Returns the datastore backend that powers this instance.
*
* The backend owns its connection(s) and exposes every datastore-agnostic
* operation through {@link IQueueBackend}. Datastore-specific escape hatches
* (e.g. the raw Redis client) live on the concrete backend implementation,
* and are exposed here when the class is parameterized on that concrete
* backend type (the default for the built-in, Redis-backed classes).
*/
getBackend() {
return this.backend;
}
createBackend() {
this.backend = this.backendFactory(this.name, this.opts, {
blocking: this.hasBlockingConnection,
});
}
/**
* Helper to easily extend Job class calls.
*/
get Job() {
return job_1.Job;
}
/**
* Emits an event. Normally used by subclasses to emit events.
*
* @param event - The emitted event.
* @param args - The arguments to pass to the event listeners.
* @returns True if the event had listeners, false otherwise.
*/
emit(event, ...args) {
try {
return super.emit(event, ...args);
}
catch (err) {
try {
return super.emit('error', err);
}
catch (err) {
// We give up if the error event also throws an exception.
console.error(err);
return false;
}
}
}
base64Name() {
return Buffer.from(this.name).toString('base64');
}
clientName(suffix = '') {
return this.backend.clientName(suffix);
}
/**
*
* Closes the connection and returns a promise that resolves when the connection is closed.
*/
async close() {
if (!this.closing) {
this.closing = this.backend.close();
}
await this.closing;
this.closed = true;
}
/**
*
* Force disconnects a connection.
*/
disconnect() {
return this.backend.disconnect();
}
async checkConnectionError(fn, delayInMs = utils_1.DELAY_TIME_5) {
try {
return await fn();
}
catch (error) {
if ((0, utils_1.isNotConnectionError)(error)) {
this.emit('error', error);
}
if (!this.closing && delayInMs) {
await (0, utils_1.delay)(delayInMs);
}
else {
return;
}
}
}
/**
* Wraps the code with telemetry and provides a span for configuration.
*
* @param spanKind - kind of the span: Producer, Consumer, Internal
* @param operation - operation name (such as add, process, etc)
* @param destination - destination name (normally the queue name)
* @param callback - code to wrap with telemetry
* @param srcPropagationMetadata - The source propagation metadata for telemetry context.
* @returns The result of the callback function.
*/
trace(spanKind, operation, destination, callback, srcPropagationMetadata) {
return (0, utils_1.trace)(this.opts.telemetry, spanKind, this.name, operation, destination, callback, srcPropagationMetadata);
}
}
exports.QueueBase = QueueBase;

View File

@@ -0,0 +1,36 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.QueueEventsProducer = void 0;
const tslib_1 = require("tslib");
const queue_base_1 = require("./queue-base");
/**
* The QueueEventsProducer class is used for publishing custom events.
*/
class QueueEventsProducer extends queue_base_1.QueueBase {
constructor(name, opts = {
connection: {},
}, backendFactory) {
super(name, Object.assign({ blockingConnection: false }, opts), backendFactory);
this.opts = opts;
}
/**
* Publish custom event to be processed in QueueEvents.
* @param argsObj - Event payload
* @param maxEvents - Max quantity of events to be saved
*/
async publishEvent(argsObj, maxEvents = 1000) {
const { eventName } = argsObj, restArgs = tslib_1.__rest(argsObj, ["eventName"]);
const fields = Object.assign({ event: eventName }, restArgs);
await this.backend.publishEvent(fields, maxEvents);
}
/**
* Closes the connection and returns a promise that resolves when the connection is closed.
*/
async close() {
if (!this.closing) {
this.closing = this.backend.close();
}
await this.closing;
}
}
exports.QueueEventsProducer = QueueEventsProducer;

134
node_modules/bullmq/dist/cjs/classes/queue-events.js generated vendored Normal file
View File

@@ -0,0 +1,134 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.QueueEvents = void 0;
const tslib_1 = require("tslib");
const utils_1 = require("../utils");
const queue_base_1 = require("./queue-base");
const ioredis_client_1 = require("./ioredis-client");
/**
* The QueueEvents class is used for listening to the global events
* emitted by a given queue.
*
* This class requires a dedicated redis connection.
*
*/
class QueueEvents extends queue_base_1.QueueBase {
constructor(name, _a = {
connection: {},
}, backendFactory) {
var { connection, autorun = true } = _a, opts = tslib_1.__rest(_a, ["connection", "autorun"]);
super(name, Object.assign(Object.assign({}, opts), { connection: (0, utils_1.isRedisInstance)(connection)
? ((0, ioredis_client_1.isIRedisClient)(connection)
? connection
: (0, ioredis_client_1.createIORedisClient)(connection)).duplicate()
: connection }), backendFactory, true);
this.running = false;
this.blocking = false;
this.opts = Object.assign({
blockingTimeout: 10000,
}, this.opts);
if (autorun) {
this.run().catch(error => this.emit('error', error));
}
}
emit(event, ...args) {
return super.emit(event, ...args);
}
off(eventName, listener) {
super.off(eventName, listener);
return this;
}
on(event, listener) {
super.on(event, listener);
return this;
}
once(event, listener) {
super.once(event, listener);
return this;
}
/**
* Manually starts running the event consumming loop. This shall be used if you do not
* use the default "autorun" option on the constructor.
*/
async run() {
if (!this.running) {
try {
this.running = true;
// TODO: Planned for deprecation as it really has no use case
await this.backend.setName(this.clientName(utils_1.QUEUE_EVENT_SUFFIX));
await this.consumeEvents();
}
catch (error) {
this.running = false;
throw error;
}
}
else {
throw new Error('Queue Events is already running.');
}
}
async consumeEvents() {
const opts = this.opts;
let id = opts.lastEventId || '$';
while (!this.closing) {
this.blocking = true;
// Cast to actual return type, see: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/44301
const data = await this.checkConnectionError(() => this.backend.readEvents(id, opts.blockingTimeout));
this.blocking = false;
if (data) {
const stream = data[0];
const events = stream[1];
for (let i = 0; i < events.length; i++) {
id = events[i][0];
const args = (0, utils_1.array2obj)(events[i][1]);
//
// TODO: we may need to have a separate stream for progress data
// to avoid this hack.
switch (args.event) {
case 'progress':
args.data = JSON.parse(args.data);
break;
case 'completed':
args.returnvalue = JSON.parse(args.returnvalue);
break;
case 'delayed':
args.delay = Number(args.delay);
break;
}
const { event } = args, restArgs = tslib_1.__rest(args, ["event"]);
if (event === 'drained') {
this.emit(event, id);
}
else {
this.emit(event, restArgs, id);
if (restArgs.jobId) {
this.emit(`${event}:${restArgs.jobId}`, restArgs, id);
}
}
}
}
}
}
/**
* Stops consuming events and close the underlying Redis connection if necessary.
*
* @returns
*/
async close() {
if (!this.closing) {
this.closing = (async () => {
try {
// Force a disconnect first to interrupt the blocking XREAD, then
// close the underlying connection.
await this.backend.disconnect();
await this.backend.close();
}
finally {
this.closed = true;
}
})();
}
return this.closing;
}
}
exports.QueueEvents = QueueEvents;

553
node_modules/bullmq/dist/cjs/classes/queue-getters.js generated vendored Normal file
View File

@@ -0,0 +1,553 @@
'use strict';
Object.defineProperty(exports, "__esModule", { value: true });
exports.QueueGetters = void 0;
const tslib_1 = require("tslib");
const queue_base_1 = require("./queue-base");
const utils_1 = require("../utils");
const enums_1 = require("../enums");
const redis_queue_backend_1 = require("./redis-queue-backend");
/**
* Escape a Prometheus label value per the text exposition format.
* https://prometheus.io/docs/instrumenting/exposition_formats/
*
* Backslashes, double quotes, and newlines must be escaped.
*/
function escapePrometheusLabelValue(value) {
return String(value)
.replace(/\\/g, '\\\\')
.replace(/"/g, '\\"')
.replace(/\n/g, '\\n');
}
/**
* Provides different getters for different aspects of a queue.
*/
class QueueGetters extends queue_base_1.QueueBase {
getJob(jobId) {
return this.Job.fromId(this, jobId);
}
commandByType(types, count, callback) {
return types.map((type) => {
type = type === 'waiting' ? 'wait' : type; // alias
const key = this.toKey(type);
switch (type) {
case 'completed':
case 'failed':
case 'delayed':
case 'prioritized':
case 'repeat':
case 'waiting-children':
return callback(key, count ? 'zcard' : 'zrange');
case 'active':
case 'wait':
return callback(key, count ? 'llen' : 'lrange');
}
});
}
sanitizeJobTypes(types) {
const currentTypes = typeof types === 'string' ? [types] : types;
if (Array.isArray(currentTypes) && currentTypes.length > 0) {
const sanitizedTypes = [...currentTypes];
return [...new Set(sanitizedTypes)];
}
return [
'active',
'completed',
'delayed',
'failed',
'prioritized',
'waiting',
'waiting-children',
];
}
/**
Returns the number of jobs waiting to be processed. This includes jobs that are
"waiting" or "delayed" or "prioritized" or "waiting-children".
*/
async count() {
const count = await this.getJobCountByTypes('waiting', 'delayed', 'prioritized', 'waiting-children');
return count;
}
/**
* Returns the time to live for a rate limited key in milliseconds.
* @param maxJobs - max jobs to be considered in rate limit state. If not passed
* it will return the remaining ttl without considering if max jobs is exceeded.
* @returns -2 if the key does not exist.
* -1 if the key exists but has no associated expire.
* @see {@link https://redis.io/commands/pttl/}
*/
async getRateLimitTtl(maxJobs) {
return this.backend.getRateLimitTtl(maxJobs);
}
/**
* Get jobId that starts debounced state.
* @deprecated use getDeduplicationJobId method
*
* @param id - debounce identifier
*/
async getDebounceJobId(id) {
return this.backend.getDeduplicationJobId(id);
}
/**
* Get jobId from deduplicated state.
*
* @param id - deduplication identifier
*/
async getDeduplicationJobId(id) {
return this.backend.getDeduplicationJobId(id);
}
/**
* Get global concurrency value.
* Returns null in case no value is set.
*/
async getGlobalConcurrency() {
const concurrency = await this.backend.getQueueMetaField('concurrency');
if (concurrency) {
return Number(concurrency);
}
return null;
}
/**
* Get global rate limit values.
* Returns null in case no value is set.
*/
async getGlobalRateLimit() {
const [max, duration] = await this.backend.getQueueMetaFields([
'max',
'duration',
]);
if (max && duration) {
return {
max: Number(max),
duration: Number(duration),
};
}
return null;
}
/**
* Job counts by type
*
* Queue#getJobCountByTypes('completed') =\> completed count
* Queue#getJobCountByTypes('completed', 'failed') =\> completed + failed count
* Queue#getJobCountByTypes('completed', 'waiting', 'failed') =\> completed + waiting + failed count
*/
async getJobCountByTypes(...types) {
const result = await this.getJobCounts(...types);
return Object.values(result).reduce((sum, count) => sum + count, 0);
}
/**
* Returns the job counts for each type specified or every list/set in the queue by default.
* @param types - the types of jobs to count. If not specified, it will return the counts for all types.
* @returns An object, key (type) and value (count)
*/
async getJobCounts(...types) {
const currentTypes = this.sanitizeJobTypes(types);
const responses = await this.backend.getCounts(currentTypes);
const counts = {};
responses.forEach((res, index) => {
counts[currentTypes[index]] = res || 0;
});
return counts;
}
/**
* Records job counts as gauge metrics for telemetry purposes.
* Each job state count is recorded with the queue name and state as attributes.
* @param types - the types of jobs to count. If not specified, it will return the counts for all types.
* @returns An object, key (type) and value (count)
*/
async recordJobCountsMetric(...types) {
var _a;
const counts = await this.getJobCounts(...types);
const meter = (_a = this.opts.telemetry) === null || _a === void 0 ? void 0 : _a.meter;
if (meter) {
const gauge = meter.createGauge(enums_1.MetricNames.QueueJobsCount, {
description: 'Number of jobs in the queue by state',
unit: '{jobs}',
});
for (const [state, jobCount] of Object.entries(counts)) {
gauge.record(jobCount, {
[enums_1.TelemetryAttributes.QueueName]: this.name,
[enums_1.TelemetryAttributes.QueueJobsState]: state,
});
}
}
return counts;
}
/**
* Get current job state.
*
* @param jobId - job identifier.
* @returns Returns one of these values:
* 'completed', 'failed', 'delayed', 'active', 'waiting', 'waiting-children', 'unknown'.
*/
getJobState(jobId) {
return this.backend.getState(jobId);
}
/**
* Get global queue configuration.
*
* @returns Returns the global queue configuration.
*/
async getMeta() {
const config = await this.backend.getQueueMeta();
const { concurrency, max, duration, paused, 'opts.maxLenEvents': maxLenEvents } = config, rest = tslib_1.__rest(config, ["concurrency", "max", "duration", "paused", 'opts.maxLenEvents']);
const parsedConfig = rest;
if (concurrency) {
parsedConfig['concurrency'] = Number(concurrency);
}
if (maxLenEvents) {
parsedConfig['maxLenEvents'] = Number(maxLenEvents);
}
if (max) {
parsedConfig['max'] = Number(max);
}
if (duration) {
parsedConfig['duration'] = Number(duration);
}
parsedConfig['paused'] = paused === '1';
return parsedConfig;
}
/**
* @returns Returns the number of jobs in completed status.
*/
getCompletedCount() {
return this.getJobCountByTypes('completed');
}
/**
* Returns the number of jobs in failed status.
*/
getFailedCount() {
return this.getJobCountByTypes('failed');
}
/**
* Returns the number of jobs in delayed status.
*/
getDelayedCount() {
return this.getJobCountByTypes('delayed');
}
/**
* Returns the number of jobs in active status.
*/
getActiveCount() {
return this.getJobCountByTypes('active');
}
/**
* Returns the number of jobs in prioritized status.
*/
getPrioritizedCount() {
return this.getJobCountByTypes('prioritized');
}
/**
* Returns the number of jobs per priority.
*/
async getCountsPerPriority(priorities) {
const uniquePriorities = [...new Set(priorities)];
const responses = await this.backend.getCountsPerPriority(uniquePriorities);
const counts = {};
responses.forEach((res, index) => {
counts[`${uniquePriorities[index]}`] = res || 0;
});
return counts;
}
/**
* Returns the number of jobs in waiting or paused statuses.
*/
getWaitingCount() {
return this.getJobCountByTypes('waiting');
}
/**
* Returns the number of jobs in waiting-children status.
*/
getWaitingChildrenCount() {
return this.getJobCountByTypes('waiting-children');
}
/**
* Returns the jobs that are in the "waiting" status.
* @param start - zero based index from where to start returning jobs.
* @param end - zero based index where to stop returning jobs.
*/
getWaiting(start = 0, end = -1) {
return this.getJobs(['waiting'], start, end, true);
}
/**
* Returns the jobs that are in the "waiting-children" status.
* I.E. parent jobs that have at least one child that has not completed yet.
* @param start - zero based index from where to start returning jobs.
* @param end - zero based index where to stop returning jobs.
*/
getWaitingChildren(start = 0, end = -1) {
return this.getJobs(['waiting-children'], start, end, true);
}
/**
* Returns the jobs that are in the "active" status.
* @param start - zero based index from where to start returning jobs.
* @param end - zero based index where to stop returning jobs.
*/
getActive(start = 0, end = -1) {
return this.getJobs(['active'], start, end, true);
}
/**
* Returns the jobs that are in the "delayed" status.
* @param start - zero based index from where to start returning jobs.
* @param end - zero based index where to stop returning jobs.
*/
getDelayed(start = 0, end = -1) {
return this.getJobs(['delayed'], start, end, true);
}
/**
* Returns the jobs that are in the "prioritized" status.
* @param start - zero based index from where to start returning jobs.
* @param end - zero based index where to stop returning jobs.
*/
getPrioritized(start = 0, end = -1) {
return this.getJobs(['prioritized'], start, end, true);
}
/**
* Returns the jobs that are in the "completed" status.
* @param start - zero based index from where to start returning jobs.
* @param end - zero based index where to stop returning jobs.
*/
getCompleted(start = 0, end = -1) {
return this.getJobs(['completed'], start, end, false);
}
/**
* Returns the jobs that are in the "failed" status.
* @param start - zero based index from where to start returning jobs.
* @param end - zero based index where to stop returning jobs.
*/
getFailed(start = 0, end = -1) {
return this.getJobs(['failed'], start, end, false);
}
/**
* Returns the qualified job ids and the raw job data (if available) of the
* children jobs of the given parent job.
* It is possible to get either the already processed children, in this case
* an array of qualified job ids and their result values will be returned,
* or the pending children, in this case an array of qualified job ids will
* be returned.
* A qualified job id is a string representing the job id in a given queue,
* for example: "bull:myqueue:jobid".
*
* @param parentId - The id of the parent job
* @param type - "processed" | "pending"
* @param opts - Options for the query.
*
* @returns an object with the following shape:
* `{ items: { id: string, v?: any, err?: string } [], jobs: JobJson[], total: number}`
*/
async getDependencies(parentId, type, start, end) {
const key = this.toKey(type == 'processed'
? `${parentId}:processed`
: `${parentId}:dependencies`);
const { items, total, jobs } = await this.backend.paginate(key, {
start,
end,
fetchJobs: true,
});
return {
items,
jobs,
total,
};
}
async getRanges(types, start = 0, end = 1, asc = false) {
const multiCommands = [];
this.commandByType(types, false, (key, command) => {
switch (command) {
case 'lrange':
multiCommands.push('lrange');
break;
case 'zrange':
multiCommands.push('zrange');
break;
}
});
const responses = await this.backend.getRanges(types, start, end, asc);
let results = [];
responses.forEach((response, index) => {
const result = response || [];
if (asc && multiCommands[index] === 'lrange') {
results = results.concat(result.reverse());
}
else {
results = results.concat(result);
}
});
return [...new Set(results)];
}
/**
* Returns the jobs that are on the given statuses (note that JobType is synonym for job status)
* @param types - the statuses of the jobs to return.
* @param start - zero based index from where to start returning jobs.
* @param end - zero based index where to stop returning jobs.
* @param asc - if true, the jobs will be returned in ascending order.
*/
async getJobs(types, start = 0, end = -1, asc = false) {
const currentTypes = this.sanitizeJobTypes(types);
let jobIds;
const backend = this.getBackend();
if (backend instanceof redis_queue_backend_1.RedisQueueBackend) {
const jobDataByType = await backend.getJobs(currentTypes, start, end, asc);
const seen = new Set();
jobIds = jobDataByType.reduce((ids, jobData) => {
for (const [jobId] of jobData || []) {
if (!seen.has(jobId)) {
seen.add(jobId);
ids.push(jobId);
}
}
return ids;
}, []);
}
else {
jobIds = await this.getRanges(currentTypes, start, end, asc);
}
const jobs = await Promise.all(jobIds.map(jobId => this.getJob(jobId)));
return jobs.filter(Boolean);
}
/**
* Returns the logs for a given Job.
* @param jobId - the id of the job to get the logs for.
* @param start - zero based index from where to start returning jobs.
* @param end - zero based index where to stop returning jobs.
* @param asc - if true, the jobs will be returned in ascending order.
*/
async getJobLogs(jobId, start = 0, end = -1, asc = true) {
return this.backend.getJobLogs(jobId, start, end, asc);
}
async baseGetClients(matcher) {
var _a;
try {
const lists = await this.backend.getClientList();
if (lists.length > 1) {
// Cluster: pick the node with the most matching clients.
const clientsPerNode = lists.map(list => this.parseClientList(list, matcher));
return clientsPerNode.reduce((prev, current) => (prev.length > current.length ? prev : current), []);
}
return this.parseClientList((_a = lists[0]) !== null && _a !== void 0 ? _a : '', matcher);
}
catch (err) {
if (!utils_1.clientCommandMessageReg.test(err.message)) {
throw err;
}
return [{ name: 'GCP does not support client list' }];
}
}
/**
* Get the worker list related to the queue. i.e. all the known
* workers that are available to process jobs for this queue.
* Note: GCP does not support SETNAME, so this call will not work
*
* @returns - Returns an array with workers info.
*/
getWorkers() {
const unnamedWorkerClientName = `${this.clientName()}`;
const namedWorkerClientName = `${this.clientName()}:w:`;
const matcher = (name) => name &&
(name === unnamedWorkerClientName ||
name.startsWith(namedWorkerClientName));
return this.baseGetClients(matcher);
}
/**
* Returns the current count of workers for the queue.
*
* getWorkersCount(): Promise<number>
*
*/
async getWorkersCount() {
const workers = await this.getWorkers();
return workers.length;
}
/**
* Get queue events list related to the queue.
* Note: GCP does not support SETNAME, so this call will not work
*
* @deprecated do not use this method, it will be removed in the future.
*
* @returns - Returns an array with queue events info.
*/
async getQueueEvents() {
const clientName = `${this.clientName()}${utils_1.QUEUE_EVENT_SUFFIX}`;
return this.baseGetClients((name) => name === clientName);
}
/**
* Get queue metrics related to the queue.
*
* This method returns the gathered metrics for the queue.
* The metrics are represented as an array of job counts
* per unit of time (1 minute).
*
* @param start - Start point of the metrics, where 0
* is the newest point to be returned.
* @param end - End point of the metrics, where -1 is the
* oldest point to be returned.
*
* @returns - Returns an object with queue metrics.
*/
async getMetrics(type, start = 0, end = -1) {
const [meta, data, count] = await this.backend.getMetrics(type, start, end);
return {
meta: {
count: parseInt(meta[0] || '0', 10),
prevTS: parseInt(meta[1] || '0', 10),
prevCount: parseInt(meta[2] || '0', 10),
},
data: data.map(point => +point || 0),
count,
};
}
parseClientList(list, matcher) {
const lines = list.split(/\r?\n/);
const clients = [];
lines.forEach((line) => {
const client = {};
const keyValues = line.split(' ');
keyValues.forEach(function (keyValue) {
const index = keyValue.indexOf('=');
const key = keyValue.substring(0, index);
const value = keyValue.substring(index + 1);
client[key] = value;
});
const name = client['name'];
if (matcher(name)) {
client['name'] = this.name;
client['rawname'] = name;
clients.push(client);
}
});
return clients;
}
/**
* Export the metrics for the queue in the Prometheus format.
* Automatically exports all the counts returned by getJobCounts().
*
* @returns - Returns a string with the metrics in the Prometheus format.
*
* @see {@link https://prometheus.io/docs/instrumenting/exposition_formats/}
**/
async exportPrometheusMetrics(globalVariables) {
const counts = await this.getJobCounts();
const metrics = [];
// Match the test's expected HELP text
metrics.push('# HELP bullmq_job_count Number of jobs in the queue by state');
metrics.push('# TYPE bullmq_job_count gauge');
const escapedQueueName = escapePrometheusLabelValue(this.name);
const variables = !globalVariables
? ''
: Object.keys(globalVariables).reduce((acc, curr) => `${acc}, ${curr}="${escapePrometheusLabelValue(globalVariables[curr])}"`, '');
for (const [state, count] of Object.entries(counts)) {
metrics.push(`bullmq_job_count{queue="${escapedQueueName}", state="${state}"${variables}} ${count}`);
}
const [completedMetrics, failedMetrics] = await Promise.all([
this.getMetrics('completed'),
this.getMetrics('failed'),
]);
metrics.push('# HELP bullmq_job_completed_total Total number of completed jobs');
metrics.push('# TYPE bullmq_job_completed_total counter');
metrics.push(`bullmq_job_completed_total{queue="${escapedQueueName}"${variables}} ${completedMetrics.meta.count}`);
metrics.push('# HELP bullmq_job_failed_total Total number of failed jobs');
metrics.push('# TYPE bullmq_job_failed_total counter');
metrics.push(`bullmq_job_failed_total{queue="${escapedQueueName}"${variables}} ${failedMetrics.meta.count}`);
return metrics.join('\n');
}
}
exports.QueueGetters = QueueGetters;

42
node_modules/bullmq/dist/cjs/classes/queue-keys.js generated vendored Normal file
View File

@@ -0,0 +1,42 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.QueueKeys = void 0;
class QueueKeys {
constructor(prefix = 'bull') {
this.prefix = prefix;
}
getKeys(name) {
const keys = {};
[
'',
'active',
'wait',
'waiting-children',
'paused',
'id',
'delayed',
'prioritized',
'stalled-check',
'completed',
'failed',
'stalled',
'repeat',
'limiter',
'meta',
'events',
'pc', // priority counter key
'marker', // marker key
'de', // deduplication key
].forEach(key => {
keys[key] = this.toKey(name, key);
});
return keys;
}
toKey(name, type) {
return `${this.getQueueQualifiedName(name)}:${type}`;
}
getQueueQualifiedName(name) {
return `${this.prefix}:${name}`;
}
}
exports.QueueKeys = QueueKeys;

599
node_modules/bullmq/dist/cjs/classes/queue.js generated vendored Normal file
View File

@@ -0,0 +1,599 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Queue = void 0;
const job_1 = require("./job");
const queue_getters_1 = require("./queue-getters");
const enums_1 = require("../enums");
const job_scheduler_1 = require("./job-scheduler");
const version_1 = require("../version");
const utils_1 = require("../utils");
/**
* Queue
*
* This class provides methods to add jobs to a queue and some other high-level
* administration such as pausing or deleting queues.
*
* @typeParam DataType - The type of the data that the job will process.
* @typeParam ResultType - The type of the result of the job.
* @typeParam NameType - The type of the name of the job.
*
* @example
*
* ```typescript
* import { Queue } from 'bullmq';
*
* interface MyDataType {
* foo: string;
* }
*
* interface MyResultType {
* bar: string;
* }
*
* const queue = new Queue<MyDataType, MyResultType, "blue" | "brown">('myQueue');
* ```
*/
class Queue extends queue_getters_1.QueueGetters {
constructor(name, opts, backendFactory) {
var _a;
super(name, Object.assign({}, opts), backendFactory);
this.token = (0, utils_1.randomUUID)();
this.libName = 'bullmq';
this.jobsOpts = (_a = opts === null || opts === void 0 ? void 0 : opts.defaultJobOptions) !== null && _a !== void 0 ? _a : {};
this.queueMetaInitialized = this.waitUntilReady()
.then(() => {
if (!this.closing && !(opts === null || opts === void 0 ? void 0 : opts.skipMetasUpdate)) {
return this.backend
.setQueueMeta(this.metaValues)
.then(() => undefined);
}
})
.catch((_err) => {
// We ignore this error to avoid warnings. The error can still
// be received by listening to event 'error'
});
}
emit(event, ...args) {
return super.emit(event, ...args);
}
off(eventName, listener) {
super.off(eventName, listener);
return this;
}
on(event, listener) {
super.on(event, listener);
return this;
}
once(event, listener) {
super.once(event, listener);
return this;
}
/**
* Returns this instance current default job options.
*/
get defaultJobOptions() {
return Object.assign({}, this.jobsOpts);
}
get metaValues() {
var _a, _b, _c, _d;
return {
'opts.maxLenEvents': (_d = (_c = (_b = (_a = this.opts) === null || _a === void 0 ? void 0 : _a.streams) === null || _b === void 0 ? void 0 : _b.events) === null || _c === void 0 ? void 0 : _c.maxLen) !== null && _d !== void 0 ? _d : 10000,
version: `${this.libName}:${version_1.version}`,
};
}
/**
* Get library version.
*
* @returns the content of the `meta.version` field, or `null` when the meta
* hash has not been populated yet (e.g. when `skipMetasUpdate` is enabled and
* no other instance has written it).
*/
async getVersion() {
var _a;
if (!((_a = this.opts) === null || _a === void 0 ? void 0 : _a.skipMetasUpdate)) {
await this.queueMetaInitialized;
}
return await this.backend.getQueueMetaField('version');
}
get jobScheduler() {
return new Promise(async (resolve) => {
if (!this._jobScheduler) {
// Share this queue's backend (same queue name/keys) with the scheduler.
this._jobScheduler = new job_scheduler_1.JobScheduler(this.name, this.opts, () => this.backend);
this._jobScheduler.on('error', this.emit.bind(this, 'error'));
}
resolve(this._jobScheduler);
});
}
/**
* Enable and set global concurrency value.
* @param concurrency - Maximum number of simultaneous jobs that the workers can handle.
* For instance, setting this value to 1 ensures that no more than one job
* is processed at any given time. If this limit is not defined, there will be no
* restriction on the number of concurrent jobs.
*/
async setGlobalConcurrency(concurrency) {
return this.backend.setQueueMeta({ concurrency });
}
/**
* Enable and set rate limit.
* @param max - Max number of jobs to process in the time period specified in `duration`
* @param duration - Time in milliseconds. During this time, a maximum of `max` jobs will be processed.
*/
async setGlobalRateLimit(max, duration) {
return this.backend.setQueueMeta({ max, duration });
}
/**
* Remove global concurrency value.
*/
async removeGlobalConcurrency() {
return this.backend.removeQueueMetaFields(['concurrency']);
}
/**
* Remove global rate limit values.
*/
async removeGlobalRateLimit() {
return this.backend.removeQueueMetaFields(['max', 'duration']);
}
/**
* Adds a new job to the queue.
*
* @param name - Name of the job to be added to the queue.
* @param data - Arbitrary data to append to the job. The value is
* serialized with `JSON.stringify` before being stored in Redis, so it
* must be JSON-serializable. The `DataType` type parameter describes the
* shape of this payload, not a runtime class: passing a class instance
* will store its enumerable own properties, but its prototype methods
* and getters/setters will not be available when a worker reads
* `job.data` back. Prefer plain objects, or implement `toJSON()` on
* the class to control how it is serialized.
* @param opts - Job options that affects how the job is going to be processed.
*/
async add(name, data, opts) {
return this.trace(enums_1.SpanKind.PRODUCER, 'add', `${this.name}.${name}`, async (span, srcPropagationMetadata) => {
var _a;
if (srcPropagationMetadata && !((_a = opts === null || opts === void 0 ? void 0 : opts.telemetry) === null || _a === void 0 ? void 0 : _a.omitContext)) {
const telemetry = {
metadata: srcPropagationMetadata,
};
opts = Object.assign(Object.assign({}, opts), { telemetry });
}
const job = await this.addJob(name, data, opts);
span === null || span === void 0 ? void 0 : span.setAttributes({
[enums_1.TelemetryAttributes.JobName]: name,
[enums_1.TelemetryAttributes.JobId]: job.id,
});
return job;
});
}
/**
* addJob is a telemetry free version of the add method, useful in order to wrap it
* with custom telemetry on subclasses.
*
* @param name - Name of the job to be added to the queue.
* @param data - Arbitrary data to append to the job.
* @param opts - Job options that affects how the job is going to be processed.
*
* @returns Job
*/
async addJob(name, data, opts) {
const jobId = opts === null || opts === void 0 ? void 0 : opts.jobId;
if (jobId == '0' || (jobId === null || jobId === void 0 ? void 0 : jobId.startsWith('0:'))) {
throw new Error("JobId cannot be '0' or start with '0:'");
}
const mergedOpts = Object.assign(Object.assign(Object.assign({}, this.jobsOpts), opts), { jobId });
const job = await this.Job.create(this, name, data, mergedOpts);
this.emit('waiting', job);
return job;
}
/**
* Adds an array of jobs to the queue. This method may be faster than adding
* one job at a time in a sequence.
*
* @param jobs - The array of jobs to add to the queue. Each job is defined by 3
* properties, 'name', 'data' and 'opts'. They follow the same signature as 'Queue.add',
* including the JSON-serialization caveat for `data` (class instances lose their
* prototype methods on the worker side).
*/
async addBulk(jobs) {
return this.trace(enums_1.SpanKind.PRODUCER, 'addBulk', this.name, async (span, srcPropagationMetadata) => {
if (span) {
span.setAttributes({
[enums_1.TelemetryAttributes.BulkNames]: jobs.map(job => job.name),
[enums_1.TelemetryAttributes.BulkCount]: jobs.length,
});
}
return await this.Job.createBulk(this, jobs.map(job => {
var _a, _b, _c, _d, _e, _f;
let telemetry = (_a = job.opts) === null || _a === void 0 ? void 0 : _a.telemetry;
if (srcPropagationMetadata) {
const omitContext = (_c = (_b = job.opts) === null || _b === void 0 ? void 0 : _b.telemetry) === null || _c === void 0 ? void 0 : _c.omitContext;
const telemetryMetadata = ((_e = (_d = job.opts) === null || _d === void 0 ? void 0 : _d.telemetry) === null || _e === void 0 ? void 0 : _e.metadata) ||
(!omitContext && srcPropagationMetadata);
if (telemetryMetadata || omitContext) {
telemetry = {
metadata: telemetryMetadata,
omitContext,
};
}
}
const mergedOpts = Object.assign(Object.assign(Object.assign({}, this.jobsOpts), job.opts), { jobId: (_f = job.opts) === null || _f === void 0 ? void 0 : _f.jobId, telemetry });
return {
name: job.name,
data: job.data,
opts: mergedOpts,
};
}));
});
}
/**
* Upserts a scheduler.
*
* A scheduler is a job factory that creates jobs at a given interval.
* Upserting a scheduler will create a new job scheduler or update an existing one.
* It will also create the first job based on the repeat options and delayed accordingly.
*
* @param key - Unique key for the repeatable job meta.
* @param repeatOpts - Repeat options
* @param jobTemplate - Job template. If provided it will be used for all the jobs
* created by the scheduler.
*
* @returns The next job to be scheduled (would normally be in delayed state).
*/
async upsertJobScheduler(jobSchedulerId, repeatOpts, jobTemplate) {
var _a, _b;
if (repeatOpts.endDate) {
if (+new Date(repeatOpts.endDate) < Date.now()) {
throw new Error('End date must be greater than current timestamp');
}
}
return (await this.jobScheduler).upsertJobScheduler(jobSchedulerId, repeatOpts, (_a = jobTemplate === null || jobTemplate === void 0 ? void 0 : jobTemplate.name) !== null && _a !== void 0 ? _a : jobSchedulerId, (_b = jobTemplate === null || jobTemplate === void 0 ? void 0 : jobTemplate.data) !== null && _b !== void 0 ? _b : {}, Object.assign(Object.assign({}, this.jobsOpts), jobTemplate === null || jobTemplate === void 0 ? void 0 : jobTemplate.opts), { override: true });
}
/**
* Pauses the processing of this queue globally.
*
* We use an atomic RENAME operation on the wait queue. Since
* we have blocking calls with BRPOPLPUSH on the wait queue, as long as the queue
* is renamed to 'paused', no new jobs will be processed (the current ones
* will run until finalized).
*
* Adding jobs requires a LUA script to check first if the paused list exist
* and in that case it will add it there instead of the wait list.
*/
async pause() {
await this.trace(enums_1.SpanKind.INTERNAL, 'pause', this.name, async () => {
await this.backend.pause(true);
this.emit('paused');
});
}
/**
* Close the queue instance.
*
*/
async close() {
await this.trace(enums_1.SpanKind.INTERNAL, 'close', this.name, async () => {
await super.close();
});
}
/**
* Overrides the rate limit to be active for the next jobs.
*
* @param expireTimeMs - expire time in ms of this rate limit.
*/
async rateLimit(expireTimeMs) {
await this.trace(enums_1.SpanKind.INTERNAL, 'rateLimit', this.name, async (span) => {
span === null || span === void 0 ? void 0 : span.setAttributes({
[enums_1.TelemetryAttributes.QueueRateLimit]: expireTimeMs,
});
await this.backend.setRateLimit(expireTimeMs);
});
}
/**
* Resumes the processing of this queue globally.
*
* The method reverses the pause operation by resuming the processing of the
* queue.
*/
async resume() {
await this.trace(enums_1.SpanKind.INTERNAL, 'resume', this.name, async () => {
await this.backend.pause(false);
this.emit('resumed');
});
}
/**
* Returns true if the queue is currently paused.
*/
async isPaused() {
return this.backend.hasQueueMetaField('paused');
}
/**
* Returns true if the queue is currently maxed.
*/
isMaxed() {
return this.backend.isMaxed();
}
/**
* Get Job Scheduler by id
*
* @param id - identifier of scheduler.
*/
async getJobScheduler(id) {
return (await this.jobScheduler).getScheduler(id);
}
/**
* Get all Job Schedulers
*
* @param start - Offset of first scheduler to return.
* @param end - Offset of last scheduler to return.
* @param asc - Determine the order in which schedulers are returned based on their
* next execution time.
*/
async getJobSchedulers(start, end, asc) {
return (await this.jobScheduler).getJobSchedulers(start, end, asc);
}
/**
*
* Get the number of job schedulers.
*
* @returns The number of job schedulers.
*/
async getJobSchedulersCount() {
return (await this.jobScheduler).getSchedulersCount();
}
/**
*
* Removes a job scheduler.
*
* @param jobSchedulerId - identifier of the job scheduler.
*
* @returns
*/
async removeJobScheduler(jobSchedulerId) {
const jobScheduler = await this.jobScheduler;
const removed = await jobScheduler.removeJobScheduler(jobSchedulerId);
return !removed;
}
/**
* Removes a debounce key.
* @deprecated use removeDeduplicationKey
*
* @param id - debounce identifier
*/
async removeDebounceKey(id) {
return this.trace(enums_1.SpanKind.INTERNAL, 'removeDebounceKey', `${this.name}`, async (span) => {
span === null || span === void 0 ? void 0 : span.setAttributes({
[enums_1.TelemetryAttributes.JobKey]: id,
});
return await this.backend.deleteDeduplicationKey(id);
});
}
/**
* Removes a deduplication key.
*
* @param id - identifier
*/
async removeDeduplicationKey(id) {
return this.trace(enums_1.SpanKind.INTERNAL, 'removeDeduplicationKey', `${this.name}`, async (span) => {
span === null || span === void 0 ? void 0 : span.setAttributes({
[enums_1.TelemetryAttributes.DeduplicationKey]: id,
});
return this.backend.deleteDeduplicationKey(id);
});
}
/**
* Removes rate limit key.
*/
async removeRateLimitKey() {
return this.backend.removeRateLimitKey();
}
/**
* Removes the given job from the queue as well as all its
* dependencies.
*
* @param jobId - The id of the job to remove
* @param opts - Options to remove a job
* @returns 1 if it managed to remove the job or 0 if the job or
* any of its dependencies were locked.
*/
async remove(jobId, { removeChildren = true } = {}) {
return this.trace(enums_1.SpanKind.INTERNAL, 'remove', this.name, async (span) => {
span === null || span === void 0 ? void 0 : span.setAttributes({
[enums_1.TelemetryAttributes.JobId]: jobId,
[enums_1.TelemetryAttributes.JobOptions]: JSON.stringify({
removeChildren,
}),
});
const code = await this.backend.remove(jobId, removeChildren);
if (code === 1) {
this.emit('removed', jobId);
}
return code;
});
}
/**
* Updates the given job's progress.
*
* @param jobId - The id of the job to update
* @param progress - Number or object to be saved as progress.
*/
async updateJobProgress(jobId, progress) {
await this.trace(enums_1.SpanKind.INTERNAL, 'updateJobProgress', this.name, async (span) => {
span === null || span === void 0 ? void 0 : span.setAttributes({
[enums_1.TelemetryAttributes.JobId]: jobId,
[enums_1.TelemetryAttributes.JobProgress]: JSON.stringify(progress),
});
await this.backend.updateProgress(jobId, progress);
this.emit('progress', jobId, progress);
});
}
/**
* Logs one row of job's log data.
*
* @param jobId - The job id to log against.
* @param logRow - String with log data to be logged.
* @param keepLogs - Max number of log entries to keep (0 for unlimited).
*
* @returns The total number of log entries for this job so far.
*/
async addJobLog(jobId, logRow, keepLogs) {
return job_1.Job.addJobLog(this, jobId, logRow, keepLogs);
}
/**
* Drains the queue, i.e., removes all jobs that are waiting
* or delayed, but not active, completed or failed.
*
* @param delayed - Pass true if it should also clean the
* delayed jobs.
*/
async drain(delayed = false) {
await this.trace(enums_1.SpanKind.INTERNAL, 'drain', this.name, async (span) => {
span === null || span === void 0 ? void 0 : span.setAttributes({
[enums_1.TelemetryAttributes.QueueDrainDelay]: delayed,
});
await this.backend.drain(delayed);
});
}
/**
* Cleans jobs from a queue. Similar to drain but keeps jobs within a certain
* grace period.
*
* @param grace - The grace period in milliseconds
* @param limit - Max number of jobs to clean
* @param type - The type of job to clean
* Possible values are completed, wait, active, paused, delayed, failed. Defaults to completed.
* @returns Id jobs from the deleted records
*/
async clean(grace, limit, type = 'completed') {
return this.trace(enums_1.SpanKind.INTERNAL, 'clean', this.name, async (span) => {
const maxCount = limit || Infinity;
const maxCountPerCall = Math.min(10000, maxCount);
const timestamp = Date.now() - grace;
let deletedCount = 0;
const deletedJobsIds = [];
// Normalize 'waiting' to 'wait' for consistency with internal Redis keys
const normalizedType = type === 'waiting' ? 'wait' : type;
while (deletedCount < maxCount) {
const jobsIds = await this.backend.cleanJobsByState(normalizedType, timestamp, maxCountPerCall);
this.emit('cleaned', jobsIds, normalizedType);
deletedCount += jobsIds.length;
deletedJobsIds.push(...jobsIds);
if (jobsIds.length < maxCountPerCall) {
break;
}
}
span === null || span === void 0 ? void 0 : span.setAttributes({
[enums_1.TelemetryAttributes.QueueGrace]: grace,
[enums_1.TelemetryAttributes.JobType]: type,
[enums_1.TelemetryAttributes.QueueCleanLimit]: maxCount,
[enums_1.TelemetryAttributes.QueueCleanCount]: deletedCount,
});
return deletedJobsIds;
});
}
/**
* Completely destroys the queue and all of its contents irreversibly.
* This method will *pause* the queue and requires that there are no
* active jobs. It is possible to bypass this requirement, i.e. not
* having active jobs using the "force" option.
*
* Note: This operation requires to iterate on all the jobs stored in the queue
* and can be slow for very large queues.
*
* @param opts - Obliterate options.
*/
async obliterate(opts) {
await this.trace(enums_1.SpanKind.INTERNAL, 'obliterate', this.name, async () => {
await this.pause();
let cursor = 0;
do {
cursor = await this.backend.obliterate(Object.assign({ force: false, count: 1000 }, opts));
} while (cursor);
});
}
/**
* Retry all the failed or completed jobs.
*
* @param opts - An object with the following properties:
* - count number to limit how many jobs will be moved to wait status per iteration,
* - state failed by default or completed.
* - timestamp from which timestamp to start moving jobs to wait status, default Date.now().
*
* @returns
*/
async retryJobs(opts = {}) {
await this.trace(enums_1.SpanKind.PRODUCER, 'retryJobs', this.name, async (span) => {
span === null || span === void 0 ? void 0 : span.setAttributes({
[enums_1.TelemetryAttributes.QueueOptions]: JSON.stringify(opts),
});
let cursor = 0;
do {
cursor = await this.backend.retryFinishedJobs(opts.state, opts.count, opts.timestamp);
} while (cursor);
});
}
/**
* Promote all the delayed jobs.
*
* @param opts - An object with the following properties:
* - count number to limit how many jobs will be moved to wait status per iteration
*
* @returns
*/
async promoteJobs(opts = {}) {
await this.trace(enums_1.SpanKind.INTERNAL, 'promoteJobs', this.name, async (span) => {
span === null || span === void 0 ? void 0 : span.setAttributes({
[enums_1.TelemetryAttributes.QueueOptions]: JSON.stringify(opts),
});
let cursor = 0;
do {
cursor = await this.backend.promoteJobs(opts.count);
} while (cursor);
});
}
/**
* Trim the event stream to an approximately maxLength.
*
* @param maxLength - The approximate maximum length, or target length, of the event stream.
*/
async trimEvents(maxLength) {
return this.trace(enums_1.SpanKind.INTERNAL, 'trimEvents', this.name, async (span) => {
span === null || span === void 0 ? void 0 : span.setAttributes({
[enums_1.TelemetryAttributes.QueueEventMaxLength]: maxLength,
});
return await this.backend.trimEvents(maxLength);
});
}
/**
* Delete old priority helper key.
*/
async removeDeprecatedPriorityKey() {
return this.backend.removeDeprecatedPriorityKey();
}
/**
* Removes orphaned job keys that are stored in the backend but are not
* referenced in any queue state set.
*
* Orphaned keys can occur in rare cases when the removal-by-max-age logic
* removes state entries without fully cleaning up the corresponding job
* data (a regression introduced in v5.66.6 via #3694).
* Under normal operation this method is
* **not needed** — it is provided only as a one-time migration helper for
* users who were affected by that specific bug and want to reclaim the
* leaked storage.
*
* How the scan is performed (its atomicity, batching and how the queue's
* state keys are discovered) is an implementation detail of the underlying
* backend.
*
* @param count - Approximate number of keys to scan per iteration (default 1000).
* @param limit - Maximum number of orphaned jobs to remove (0 = unlimited).
* When set, the method returns as soon as the limit is reached.
* Users with a very large number of orphans can call this method
* in a loop: `while (await queue.removeOrphanedJobs(1000, 10000)) {}`
* @returns The total number of orphaned jobs that were removed.
*/
async removeOrphanedJobs(count = 1000, limit = 0) {
return this.backend.removeOrphanedJobs(count, limit);
}
}
exports.Queue = Queue;

View File

@@ -0,0 +1,540 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RedisConnection = void 0;
const tslib_1 = require("tslib");
const events_1 = require("events");
const ioredis_1 = require("ioredis");
const utils_1 = require("../utils");
const version_1 = require("../version");
const scripts = require("../scripts");
const ioredis_client_1 = require("./ioredis-client");
const connection_closed_error_1 = require("./errors/connection-closed-error");
const overrideMessage = [
'BullMQ: WARNING! Your redis options maxRetriesPerRequest must be null',
'and will be overridden by BullMQ.',
].join(' ');
const deprecationMessage = 'BullMQ: Your redis options maxRetriesPerRequest must be null.';
const clusterReconnectPromise = Symbol('bullmqClusterReconnectPromise');
const clusterPatchedForBlocking = Symbol('bullmqClusterPatchedForBlocking');
const clusterOriginalBzpopmin = Symbol('bullmqClusterOriginalBzpopmin');
const clusterWrappedBzpopmin = Symbol('bullmqClusterWrappedBzpopmin');
const clusterPatchRefCount = Symbol('bullmqClusterPatchRefCount');
const clusterClosingRefCount = Symbol('bullmqClusterClosingRefCount');
// Hard cap on a single cluster reconnect attempt. Without this, a hung
// `client.connect()` (e.g. ioredis Cluster cannot recover and slot refresh
// keeps retrying internally) leaves `clusterReconnectPromise` pinned forever
// and every subsequent bzpopmin call awaits the same dead promise, deadlocking
// the worker. 30s is comfortably above the default ioredis slotsRefreshTimeout
// while bounded enough that wedged workers recover within one or two ticks.
const DEFAULT_CLUSTER_RECONNECT_TIMEOUT_MS = 30000;
class RedisConnection extends events_1.EventEmitter {
constructor(opts, extraOptions) {
super();
this.extraOptions = extraOptions;
this.capabilities = {
canDoubleTimeout: false,
canBlockFor1Ms: true,
};
this.status = 'initializing';
this.dbType = 'redis';
this.packageVersion = version_1.version;
this.disabledBlockingClusterReconnect = false;
// Set extra options defaults
this.extraOptions = Object.assign({ shared: false, blocking: true, skipVersionCheck: false, skipWaitingForReady: false, clusterReconnectTimeoutMs: DEFAULT_CLUSTER_RECONNECT_TIMEOUT_MS }, extraOptions);
if (!(0, utils_1.isRedisInstance)(opts)) {
this.checkBlockingOptions(overrideMessage, opts);
this.opts = Object.assign({ port: 6379, host: '127.0.0.1', retryStrategy: function (times) {
return Math.max(Math.min(Math.exp(times), 20000), 1000);
} }, opts);
if (this.extraOptions.blocking) {
this.opts.maxRetriesPerRequest = null;
}
}
else {
// Wrap raw ioredis instances in the IRedisClient adapter if not already wrapped
this._client = (0, ioredis_client_1.isIRedisClient)(opts)
? opts
: (0, ioredis_client_1.createIORedisClient)(opts);
// Test if the redis instance is using keyPrefix
// and if so, throw an error.
if (this._client.options.keyPrefix) {
throw new Error('BullMQ: ioredis does not support ioredis prefixes, use the prefix option instead.');
}
if (this._client.isCluster) {
this.opts = this._client.options.redisOptions;
}
else {
this.opts = this._client.options;
}
this.checkBlockingOptions(deprecationMessage, this.opts, true);
}
this.skipVersionCheck =
(extraOptions === null || extraOptions === void 0 ? void 0 : extraOptions.skipVersionCheck) ||
!!(this.opts && this.opts.skipVersionCheck);
this.handleClientError = (err) => {
this.emit('error', err);
};
this.handleClientClose = () => {
this.emit('close');
};
this.handleClientReady = () => {
this.emit('ready');
};
this.initializing = this.init();
this.initializing.catch(err => {
// Only emit if there is an `error` listener attached. `EventEmitter.emit`
// throws when emitting `error` with no listeners, which would surface as
// an unhandled rejection — e.g. when the connection is force-closed during
// shutdown while its version-check `INFO` command is still in flight and
// rejects with "Connection is closed". The init error is still propagated
// to anything awaiting `initializing` (the `client` getter and `close`).
if (this.listenerCount('error') > 0) {
this.emit('error', err);
}
});
}
checkBlockingOptions(msg, options, throwError = false) {
if (this.extraOptions.blocking && options && options.maxRetriesPerRequest) {
if (throwError) {
throw new Error(msg);
}
else {
console.error(msg);
}
}
}
/**
* Waits for a redis client to be ready.
* @param redis - client
*/
static async waitUntilReady(client) {
if (client.status === 'ready') {
return;
}
// ioredis Cluster reports 'connect' as its connected status instead of
// 'ready' that standalone Redis uses. Treat it as already connected so we
// don't hang waiting for a 'ready' event that will never fire and end up
// throwing a spurious "Connection is closed" error during disconnect.
if (client.status === 'connect' && (0, utils_1.isRedisCluster)(client)) {
return;
}
if (client.status === 'wait') {
return client.connect();
}
if (client.status === 'end') {
throw new connection_closed_error_1.ConnectionClosedError(connection_closed_error_1.CONNECTION_CLOSED_ERROR_MSG);
}
let handleReady;
let handleEnd;
let handleError;
try {
await new Promise((resolve, reject) => {
let lastError;
handleError = (err) => {
lastError = err;
};
handleReady = () => {
resolve();
};
handleEnd = () => {
if (client.status !== 'end') {
reject(lastError ||
new connection_closed_error_1.ConnectionClosedError(connection_closed_error_1.CONNECTION_CLOSED_ERROR_MSG));
}
else {
if (lastError) {
reject(lastError);
}
else {
// when custom 'end' status is set we already closed
resolve();
}
}
};
(0, utils_1.increaseMaxListeners)(client, 3);
client.once('ready', handleReady);
client.on('end', handleEnd);
client.once('error', handleError);
});
}
finally {
client.removeListener('end', handleEnd);
client.removeListener('error', handleError);
client.removeListener('ready', handleReady);
(0, utils_1.decreaseMaxListeners)(client, 3);
}
}
get client() {
return this.initializing;
}
loadCommands(packageVersion, providedScripts) {
const finalScripts = providedScripts || scripts;
for (const property in finalScripts) {
// Only define the command if not already defined
const commandName = `${finalScripts[property].name}:${packageVersion}`;
if (!this._client[commandName]) {
this._client.defineCommand(commandName, {
numberOfKeys: finalScripts[property].keys,
lua: finalScripts[property].content,
});
}
}
}
async init() {
if (!this._client) {
if (RedisConnection.clientFactory) {
this._client = RedisConnection.clientFactory(this.opts);
}
else {
const _a = this.opts, { url } = _a, rest = tslib_1.__rest(_a, ["url"]);
const ioredisClient = url ? new ioredis_1.default(url, rest) : new ioredis_1.default(rest);
this._client = (0, ioredis_client_1.createIORedisClient)(ioredisClient);
}
}
(0, utils_1.increaseMaxListeners)(this._client, 3);
this._client.on('error', this.handleClientError);
// ioredis treats connection errors as a different event ('close')
this._client.on('close', this.handleClientClose);
this._client.on('ready', this.handleClientReady);
this.patchBlockingClusterClient();
if (!this.extraOptions.skipWaitingForReady) {
await RedisConnection.waitUntilReady(this._client);
}
this.loadCommands(this.packageVersion);
if (this._client['status'] !== 'end') {
const versionResult = await this.getRedisVersionAndType();
this.version = versionResult.version;
this.dbType = versionResult.databaseType;
if (this.skipVersionCheck !== true && !this.closing) {
if ((0, utils_1.isRedisVersionLowerThan)(this.version, RedisConnection.minimumVersion, this.dbType)) {
throw new Error(`Redis version needs to be greater or equal than ${RedisConnection.minimumVersion} ` +
`Current: ${this.version}`);
}
if ((0, utils_1.isRedisVersionLowerThan)(this.version, RedisConnection.recommendedMinimumVersion, this.dbType)) {
console.warn(`It is highly recommended to use a minimum Redis version of ${RedisConnection.recommendedMinimumVersion}
Current: ${this.version}`);
}
}
this.capabilities = {
canDoubleTimeout: !(0, utils_1.isRedisVersionLowerThan)(this.version, '6.0.0', this.dbType),
canBlockFor1Ms: !(0, utils_1.isRedisVersionLowerThan)(this.version, '7.0.8', this.dbType),
};
this.status = 'ready';
}
return this._client;
}
patchBlockingClusterClient() {
var _a;
const client = this._client;
const blockingClient = client;
if (!this.extraOptions.blocking ||
!(0, utils_1.isRedisCluster)(client) ||
typeof blockingClient.bzpopmin !== 'function') {
return;
}
const reconnectTimeoutMs = (_a = this.extraOptions.clusterReconnectTimeoutMs) !== null && _a !== void 0 ? _a : DEFAULT_CLUSTER_RECONNECT_TIMEOUT_MS;
blockingClient[clusterPatchRefCount] =
(blockingClient[clusterPatchRefCount] || 0) + 1;
this.patchedBlockingClusterClient = blockingClient;
if (blockingClient[clusterPatchedForBlocking]) {
return;
}
const bzpopmin = blockingClient.bzpopmin;
const wrappedBzpopmin = async (...args) => {
await RedisConnection.reconnectClusterIfNeeded(blockingClient, reconnectTimeoutMs);
try {
return await bzpopmin.apply(blockingClient, args);
}
catch (error) {
const commandError = error;
if (RedisConnection.shouldReconnectClusterAfterError(blockingClient, commandError)) {
try {
await RedisConnection.reconnectCluster(blockingClient, reconnectTimeoutMs);
}
catch (_a) {
// Preserve the original command failure if best-effort recovery fails.
}
}
throw commandError;
}
};
blockingClient[clusterOriginalBzpopmin] = bzpopmin;
blockingClient[clusterWrappedBzpopmin] = wrappedBzpopmin;
blockingClient[clusterPatchedForBlocking] = true;
blockingClient.bzpopmin = wrappedBzpopmin;
}
disableBlockingClusterReconnect() {
const client = this.patchedBlockingClusterClient;
if (!client || this.disabledBlockingClusterReconnect) {
return;
}
client[clusterClosingRefCount] = (client[clusterClosingRefCount] || 0) + 1;
this.disabledBlockingClusterReconnect = true;
}
releaseBlockingClusterClientPatch() {
const client = this.patchedBlockingClusterClient;
if (!client) {
return;
}
if (this.disabledBlockingClusterReconnect) {
const closingRefCount = (client[clusterClosingRefCount] || 1) - 1;
if (closingRefCount > 0) {
client[clusterClosingRefCount] = closingRefCount;
}
else {
delete client[clusterClosingRefCount];
}
this.disabledBlockingClusterReconnect = false;
}
const patchRefCount = (client[clusterPatchRefCount] || 1) - 1;
if (patchRefCount > 0) {
client[clusterPatchRefCount] = patchRefCount;
this.patchedBlockingClusterClient = undefined;
return;
}
if (client[clusterOriginalBzpopmin] &&
client.bzpopmin === client[clusterWrappedBzpopmin]) {
client.bzpopmin = client[clusterOriginalBzpopmin];
}
delete client[clusterPatchRefCount];
delete client[clusterClosingRefCount];
delete client[clusterOriginalBzpopmin];
delete client[clusterWrappedBzpopmin];
delete client[clusterPatchedForBlocking];
this.patchedBlockingClusterClient = undefined;
}
static isClusterWithEmptyNodes(client) {
return typeof client.nodes === 'function' && client.nodes().length === 0;
}
static isReconnectingDisabled(client) {
const patchRefCount = client[clusterPatchRefCount] || 0;
const closingRefCount = client[clusterClosingRefCount] || 0;
return (patchRefCount === 0 ||
closingRefCount >= patchRefCount ||
client.status === 'end' ||
client.status === 'closing');
}
static async reconnectClusterIfNeeded(client, timeoutMs) {
if (!RedisConnection.isReconnectingDisabled(client) &&
RedisConnection.isClusterWithEmptyNodes(client)) {
await RedisConnection.reconnectCluster(client, timeoutMs);
}
}
static shouldReconnectClusterAfterError(client, error) {
var _a, _b;
if (RedisConnection.isReconnectingDisabled(client)) {
return false;
}
const message = [
error.message,
(_a = error.cause) === null || _a === void 0 ? void 0 : _a.message,
(_b = error.lastNodeError) === null || _b === void 0 ? void 0 : _b.message,
].join(' ');
return (RedisConnection.isClusterWithEmptyNodes(client) ||
/Command timed out|Failed to refresh slots cache/i.test(message));
}
static async reconnectCluster(client, timeoutMs) {
if (RedisConnection.isReconnectingDisabled(client)) {
return;
}
if (!client[clusterReconnectPromise]) {
client[clusterReconnectPromise] =
RedisConnection.connectClusterWithTimeout(client, timeoutMs).finally(() => {
client[clusterReconnectPromise] = null;
});
}
await client[clusterReconnectPromise];
}
// Disconnects and reconnects a cluster client, racing connect() against a
// hard cap so a hung reconnect cannot pin `clusterReconnectPromise`
// indefinitely. On timeout, the underlying connect() is left running
// (ioredis Cluster handles its own retry/state); we simply stop awaiting it.
// The next bzpopmin call's `reconnectClusterIfNeeded` check will trigger a
// fresh reconnect if the pool is still empty.
static async connectClusterWithTimeout(client, timeoutMs) {
client.disconnect(false);
let timeoutHandle;
try {
await Promise.race([
client.connect(),
new Promise((_, reject) => {
var _a;
timeoutHandle = setTimeout(() => {
reject(new connection_closed_error_1.ConnectionClosedError(`BullMQ: cluster reconnect timed out after ${timeoutMs}ms`));
}, timeoutMs);
// Don't keep the event loop alive solely for this timer.
(_a = timeoutHandle.unref) === null || _a === void 0 ? void 0 : _a.call(timeoutHandle);
}),
]);
}
finally {
if (timeoutHandle) {
clearTimeout(timeoutHandle);
}
}
}
async disconnect(wait = true) {
const client = await this.client;
if (client.status !== 'end') {
let _resolve, _reject;
if (!wait) {
return client.disconnect();
}
const disconnecting = new Promise((resolve, reject) => {
(0, utils_1.increaseMaxListeners)(client, 2);
client.once('end', resolve);
client.once('error', reject);
_resolve = resolve;
_reject = reject;
});
client.disconnect();
try {
await disconnecting;
}
finally {
(0, utils_1.decreaseMaxListeners)(client, 2);
client.removeListener('end', _resolve);
client.removeListener('error', _reject);
}
}
}
async reconnect() {
const client = await this.client;
for (;;) {
if (client.status === 'ready' ||
(client.status === 'connect' && (0, utils_1.isRedisCluster)(client))) {
return;
}
if (client.status === 'wait' || client.status === 'end') {
return client.connect();
}
try {
await RedisConnection.waitUntilReady(client);
}
catch (error) {
if (!['end', 'connecting', 'connect', 'reconnecting'].includes(client.status)) {
throw error;
}
}
}
}
async close(force = false) {
var _a;
if (!this.closing) {
const status = this.status;
this.status = 'closing';
this.closing = true;
this.disableBlockingClusterReconnect();
try {
if (status === 'ready') {
// Not sure if we need to wait for this
await this.initializing;
}
if (!this.extraOptions.shared) {
if (status == 'initializing' || force) {
// If we have not still connected to Redis, we need to disconnect.
this._client.disconnect();
// Suppress any rejection from the in-flight init() so it doesn't
// become an unhandled rejection after we close the connection.
(_a = this.initializing) === null || _a === void 0 ? void 0 : _a.catch(() => { });
}
else {
await this._client.quit();
}
// As IORedis does not update this status properly, we do it ourselves.
this._client['status'] = 'end';
}
}
catch (error) {
if ((0, utils_1.isNotConnectionError)(error)) {
throw error;
}
}
finally {
this.releaseBlockingClusterClientPatch();
this._client.off('error', this.handleClientError);
this._client.off('close', this.handleClientClose);
this._client.off('ready', this.handleClientReady);
(0, utils_1.decreaseMaxListeners)(this._client, 3);
this.removeAllListeners();
this.status = 'closed';
}
}
}
async getRedisVersionAndType() {
if (this.skipVersionCheck) {
return {
version: RedisConnection.minimumVersion,
databaseType: 'redis',
};
}
const doc = await this._client.info();
const redisPrefix = 'redis_version:';
const maxMemoryPolicyPrefix = 'maxmemory_policy:';
const lines = doc.split(/\r?\n/);
let redisVersion;
let databaseType = 'redis';
// Detect database type from server info
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// Check for Dragonfly
if (line.includes('dragonfly_version:') ||
line.includes('server:Dragonfly')) {
databaseType = 'dragonfly';
// For Dragonfly, extract version from dragonfly_version field
if (line.indexOf('dragonfly_version:') === 0) {
redisVersion = line.substr('dragonfly_version:'.length);
}
}
// Check for Valkey
else if (line.includes('valkey_version:') ||
line.includes('server:Valkey')) {
databaseType = 'valkey';
// For Valkey, extract version from valkey_version field
if (line.indexOf('valkey_version:') === 0) {
redisVersion = line.substr('valkey_version:'.length);
}
}
// Standard Redis version detection
else if (line.indexOf(redisPrefix) === 0) {
redisVersion = line.substr(redisPrefix.length);
// Keep Redis as default unless we find evidence of other databases above
if (databaseType === 'redis') {
databaseType = 'redis';
}
}
if (line.indexOf(maxMemoryPolicyPrefix) === 0) {
const maxMemoryPolicy = line.substr(maxMemoryPolicyPrefix.length);
if (maxMemoryPolicy !== 'noeviction') {
console.warn(`IMPORTANT! Eviction policy is ${maxMemoryPolicy}. It should be "noeviction"`);
}
}
}
// Fallback version detection if specific database version field wasn't found
if (!redisVersion) {
// Try to find any version field as fallback
for (const line of lines) {
if (line.includes('version:')) {
const parts = line.split(':');
if (parts.length >= 2) {
redisVersion = parts[1];
break;
}
}
}
}
return {
version: redisVersion || RedisConnection.minimumVersion,
databaseType,
};
}
get redisVersion() {
return this.version;
}
get databaseType() {
return this.dbType;
}
}
exports.RedisConnection = RedisConnection;
RedisConnection.minimumVersion = '5.0.0';
RedisConnection.recommendedMinimumVersion = '6.2.0';

File diff suppressed because it is too large Load Diff

161
node_modules/bullmq/dist/cjs/classes/sandbox.js generated vendored Normal file
View File

@@ -0,0 +1,161 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const enums_1 = require("../enums");
const sandbox = (processFile, childPool) => {
return async function process(job, token, signal) {
let child;
let msgHandler;
let exitHandler;
let abortHandler;
try {
const done = new Promise((resolve, reject) => {
const initChild = async () => {
try {
exitHandler = (exitCode, signal) => {
reject(new Error('Unexpected exit code: ' + exitCode + ' signal: ' + signal));
};
child = await childPool.retain(processFile);
child.on('exit', exitHandler);
msgHandler = async (msg) => {
var _a, _b, _c, _d, _e, _f;
try {
switch (msg.cmd) {
case enums_1.ParentCommand.Completed:
resolve(msg.value);
break;
case enums_1.ParentCommand.Failed:
case enums_1.ParentCommand.Error: {
const err = new Error();
// ParentCommand.Failed carries the error under `value`,
// while ParentCommand.Error carries it under `err`. Read
// from either key so the failure reason is never lost.
Object.assign(err, (_a = msg.value) !== null && _a !== void 0 ? _a : msg.err);
reject(err);
break;
}
case enums_1.ParentCommand.Progress:
await job.updateProgress(msg.value);
break;
case enums_1.ParentCommand.Log:
await job.log(msg.value);
break;
case enums_1.ParentCommand.MoveToDelayed:
await job.moveToDelayed((_b = msg.value) === null || _b === void 0 ? void 0 : _b.timestamp, (_c = msg.value) === null || _c === void 0 ? void 0 : _c.token);
break;
case enums_1.ParentCommand.MoveToWait:
await job.moveToWait((_d = msg.value) === null || _d === void 0 ? void 0 : _d.token);
break;
case enums_1.ParentCommand.MoveToWaitingChildren:
{
const value = await job.moveToWaitingChildren((_e = msg.value) === null || _e === void 0 ? void 0 : _e.token, (_f = msg.value) === null || _f === void 0 ? void 0 : _f.opts);
child.send({
requestId: msg.requestId,
cmd: enums_1.ChildCommand.MoveToWaitingChildrenResponse,
value,
});
}
break;
case enums_1.ParentCommand.Update:
await job.updateData(msg.value);
break;
case enums_1.ParentCommand.GetChildrenValues:
{
const value = await job.getChildrenValues();
child.send({
requestId: msg.requestId,
cmd: enums_1.ChildCommand.GetChildrenValuesResponse,
value,
});
}
break;
case enums_1.ParentCommand.GetIgnoredChildrenFailures:
{
const value = await job.getIgnoredChildrenFailures();
child.send({
requestId: msg.requestId,
cmd: enums_1.ChildCommand.GetIgnoredChildrenFailuresResponse,
value,
});
}
break;
case enums_1.ParentCommand.GetDependenciesCount:
{
const value = await job.getDependenciesCount(msg.value);
child.send({
requestId: msg.requestId,
cmd: enums_1.ChildCommand.GetDependenciesCountResponse,
value,
});
}
break;
case enums_1.ParentCommand.GetDependencies:
{
const value = await job.getDependencies(msg.value);
child.send({
requestId: msg.requestId,
cmd: enums_1.ChildCommand.GetDependenciesResponse,
value,
});
}
break;
}
}
catch (err) {
reject(err);
}
};
child.on('message', msgHandler);
child.send({
cmd: enums_1.ChildCommand.Start,
job: job.asJSONSandbox(),
token,
});
if (signal) {
abortHandler = () => {
try {
child.send({
cmd: enums_1.ChildCommand.Cancel,
value: signal.reason,
});
}
catch (_a) {
// Child process may have already exited
}
};
if (signal.aborted) {
abortHandler();
}
else {
signal.addEventListener('abort', abortHandler, { once: true });
}
}
}
catch (error) {
reject(error);
}
};
initChild();
});
await done;
return done;
}
finally {
// Note: There is a potential race where the signal is aborted between
// `await done` and this cleanup. This is safe because:
// 1. abortHandler has a try-catch for child process already exited
// 2. The listener is added with `once: true`, so it fires at most once
// 3. removeEventListener here is defensive cleanup only
if (signal && abortHandler) {
signal.removeEventListener('abort', abortHandler);
}
if (child) {
child.off('message', msgHandler);
child.off('exit', exitHandler);
if (child.exitCode === null && child.signalCode === null) {
childPool.release(child);
}
}
}
};
};
exports.default = sandbox;

View File

@@ -0,0 +1,979 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createValkeyGlideClient = createValkeyGlideClient;
const crypto_1 = require("crypto");
const events_1 = require("events");
const stream_1 = require("stream");
const connection_closed_error_1 = require("./errors/connection-closed-error");
const GLIDE_STRING_DECODER = 1;
function createCompatibilityBatch() {
return {
commands: [],
customCommand(args) {
this.commands.push(args);
},
};
}
function createGlideBatch(isAtomic) {
try {
const { Batch } = require('@valkey/valkey-glide');
return typeof Batch === 'function' ? new Batch(isAtomic) : null;
}
catch (_a) {
return null;
}
}
function isBuffer(value) {
return Buffer.isBuffer(value);
}
function toGlideArg(value) {
if (isBuffer(value)) {
return value;
}
if (value === undefined || value === null) {
return '';
}
return String(value);
}
function toStringValue(value) {
if (isBuffer(value)) {
return value.toString();
}
return String(value);
}
function isKeyValueArray(value) {
return (Array.isArray(value) &&
value.every(item => item && typeof item === 'object' && 'key' in item && 'value' in item));
}
function normalizeScriptArgs(args) {
return args.length === 1 && Array.isArray(args[0]) ? args[0] : args;
}
function normalizeHashReply(reply) {
if (!reply) {
return {};
}
if (isKeyValueArray(reply)) {
return reply.reduce((acc, item) => {
acc[toStringValue(item.key)] = toStringValue(item.value);
return acc;
}, {});
}
if (reply instanceof Map) {
const out = {};
for (const [key, value] of reply.entries()) {
out[toStringValue(key)] = toStringValue(value);
}
return out;
}
if (Array.isArray(reply)) {
const out = {};
for (let i = 0; i < reply.length; i += 2) {
out[toStringValue(reply[i])] = toStringValue(reply[i + 1]);
}
return out;
}
if (typeof reply === 'object') {
const out = {};
for (const [key, value] of Object.entries(reply)) {
out[key] = toStringValue(value);
}
return out;
}
return {};
}
function flattenFieldPairs(fields) {
if (!fields) {
return [];
}
if (Array.isArray(fields)) {
if (isKeyValueArray(fields)) {
return fields.flatMap(pair => [
toStringValue(pair.key),
toStringValue(pair.value),
]);
}
if (fields.every(item => Array.isArray(item) && item.length === 2)) {
return fields.flatMap(pair => {
const [field, value] = pair;
return [toStringValue(field), toStringValue(value)];
});
}
return fields.map(item => toStringValue(item));
}
if (typeof fields === 'object') {
return Object.entries(fields).flatMap(([field, value]) => [field, toStringValue(value)]);
}
return [];
}
function normalizeXReadReply(reply) {
if (!reply) {
return null;
}
if (Array.isArray(reply) &&
reply.every(stream => Array.isArray(stream) &&
stream.length === 2 &&
typeof stream[0] !== 'undefined' &&
Array.isArray(stream[1]))) {
return reply;
}
if (!isKeyValueArray(reply)) {
return reply;
}
return reply.map(streamEntry => {
const streamName = toStringValue(streamEntry.key);
const messages = isKeyValueArray(streamEntry.value)
? streamEntry.value.map(message => [
toStringValue(message.key),
flattenFieldPairs(message.value),
])
: [];
return [streamName, messages];
});
}
function normalizeScanReply(reply) {
if (Array.isArray(reply) && reply.length >= 2) {
const [cursor, keys] = reply;
const normalizedKeys = Array.isArray(keys)
? keys.map(key => toStringValue(key))
: [];
return [toStringValue(cursor), normalizedKeys];
}
if (reply && typeof reply === 'object') {
const result = reply;
if (result.cursor !== undefined && Array.isArray(result.keys)) {
return [
toStringValue(result.cursor),
result.keys.map(key => toStringValue(key)),
];
}
}
return ['0', []];
}
function normalizeXRangeReply(reply) {
if (!reply) {
return [];
}
if (Array.isArray(reply) &&
reply.every(entry => Array.isArray(entry) &&
entry.length === 2 &&
typeof entry[0] !== 'undefined' &&
Array.isArray(entry[1]))) {
return reply.map(([id, fields]) => [
toStringValue(id),
fields.map((field) => toStringValue(field)),
]);
}
if (!isKeyValueArray(reply)) {
return [];
}
return reply.map(entry => [
toStringValue(entry.key),
flattenFieldPairs(entry.value),
]);
}
function normalizeSortedSetReply(reply, withScores = false) {
if (!Array.isArray(reply)) {
return [];
}
if (!withScores) {
return reply.map(item => toStringValue(item));
}
if (isKeyValueArray(reply)) {
return reply.flatMap(entry => [
toStringValue(entry.key),
toStringValue(entry.value),
]);
}
if (reply.every(item => Array.isArray(item) && item.length >= 2 && item[0] !== undefined)) {
return reply.flatMap(item => [
toStringValue(item[0]),
toStringValue(item[1]),
]);
}
return reply.map(item => toStringValue(item));
}
function isBlockingCommand(args) {
const [command, ...rest] = args;
const name = toStringValue(command).toUpperCase();
if (name === 'BZPOPMIN') {
return true;
}
if (name !== 'XREAD') {
return false;
}
for (let i = 0; i < rest.length; i += 2) {
if (toStringValue(rest[i]).toUpperCase() === 'BLOCK') {
return true;
}
}
return false;
}
function createValkeyGlideClient(client) {
return new ValkeyGlideAdapter(client);
}
class ValkeyGlideAdapter extends events_1.EventEmitter {
constructor(rawOrPromise, connectionName) {
super();
this.connectionName = connectionName;
this.scripts = new Map();
this.scriptsBySha = new Map();
this.scriptLoadPromises = new Map();
this.readyEmitted = false;
this.closed = false;
this.operationChain = Promise.resolve();
this.activeBlockingCommands = 0;
if (rawOrPromise instanceof Promise) {
this.rawPromise = rawOrPromise;
void this.connect().catch(error => this.emit('error', error));
}
else {
this.raw = rawOrPromise;
}
}
get status() {
if (this.statusOverride) {
return this.statusOverride;
}
if (this.closed) {
return 'end';
}
if (this.readyEmitted) {
return 'ready';
}
return 'wait';
}
set status(value) {
if (value === 'end') {
this.disconnect();
}
this.statusOverride = value;
}
get isCluster() {
var _a, _b, _c;
const name = (_c = (_b = (_a = this.raw) === null || _a === void 0 ? void 0 : _a.constructor) === null || _b === void 0 ? void 0 : _b.name) !== null && _c !== void 0 ? _c : '';
return name.includes('Cluster');
}
get options() {
var _a, _b, _c, _d;
return (_d = (_b = (_a = this.raw) === null || _a === void 0 ? void 0 : _a.config) !== null && _b !== void 0 ? _b : (_c = this.raw) === null || _c === void 0 ? void 0 : _c.options) !== null && _d !== void 0 ? _d : {};
}
set options(_value) {
// no-op
}
ensureOpen() {
if (this.closed) {
throw new connection_closed_error_1.ConnectionClosedError();
}
}
normalizeError(error) {
if (error instanceof connection_closed_error_1.ConnectionClosedError ||
(error instanceof Error && error.name === 'ClosingError')) {
throw new connection_closed_error_1.ConnectionClosedError(error.message, error);
}
throw error;
}
async ensureRaw() {
if (this.raw) {
return this.raw;
}
if (!this.rawPromise) {
throw new Error('BullMQ: Valkey Glide client not initialized - missing raw client ' +
'and client promise. Please report this as a bug.');
}
this.raw = await this.rawPromise;
return this.raw;
}
async runSerialized(fn) {
this.ensureOpen();
const previous = this.operationChain;
let release;
this.operationChain = new Promise(resolve => {
release = resolve;
});
await previous;
this.ensureOpen();
try {
const raw = await this.ensureRaw();
this.ensureOpen();
return await fn(raw);
}
catch (error) {
this.normalizeError(error);
}
finally {
release();
}
}
async runRawCommand(args, options) {
const blockingCommand = isBlockingCommand(args);
return this.runSerialized(async (raw) => {
if (blockingCommand) {
this.activeBlockingCommands++;
}
try {
return await raw.customCommand(args, options);
}
finally {
if (blockingCommand) {
this.activeBlockingCommands--;
}
}
});
}
ensureScriptLoaded(script) {
let pendingLoad = this.scriptLoadPromises.get(script.sha);
if (!pendingLoad) {
pendingLoad = this.runRawCommand(['SCRIPT', 'LOAD', script.lua])
.then(() => { })
.catch(() => {
// Ignore script preload errors here – runCommand has NOSCRIPT fallback
// for non-transactional usage, and transactional callers will surface
// the underlying Redis error if the script still is not available.
});
this.scriptLoadPromises.set(script.sha, pendingLoad);
}
return pendingLoad;
}
async applyConnectionNameIfNeeded() {
if (!this.connectionName) {
return;
}
await this.runRawCommand(['CLIENT', 'SETNAME', this.connectionName], {
decoder: GLIDE_STRING_DECODER,
});
}
async clearConnectionNameIfNeeded(raw) {
if (!this.connectionName) {
return;
}
try {
await raw.customCommand(['CLIENT', 'SETNAME', ''], {
decoder: GLIDE_STRING_DECODER,
});
}
catch (_a) {
// ignore
}
}
async recreateRaw() {
var _a, _b;
const raw = await this.ensureRaw();
const clientConstructor = raw.constructor;
const createClient = (_a = clientConstructor === null || clientConstructor === void 0 ? void 0 : clientConstructor.createClient) === null || _a === void 0 ? void 0 : _a.bind(clientConstructor);
const config = (_b = raw.config) !== null && _b !== void 0 ? _b : raw.options;
if (!createClient || !config) {
throw new Error('BullMQ: Cannot recreate Valkey Glide client: missing createClient() method or ' +
'config object. Ensure the client was created via GlideClient.createClient() ' +
'or GlideClusterClient.createClient().');
}
this.raw = await createClient(config);
// Glide closes clients permanently, so a recreated raw connection starts
// with an empty server-side script cache.
this.scriptLoadPromises.clear();
}
async connect() {
if (this.connecting) {
return this.connecting;
}
this.connecting = (async () => {
if (this.closed && this.raw) {
await this.recreateRaw();
}
this.closed = false;
this.statusOverride = undefined;
await this.ensureRaw();
try {
await this.applyConnectionNameIfNeeded();
}
catch (error) {
if (this.closed && error instanceof connection_closed_error_1.ConnectionClosedError) {
return;
}
throw error;
}
if (this.closed) {
return;
}
this.readyEmitted = true;
this.emit('ready');
})().finally(() => {
this.connecting = undefined;
});
return this.connecting;
}
closeRaw() {
if (!this.closingPromise) {
const closeRaw = () => {
if (!this.raw) {
return;
}
try {
this.raw.close();
}
catch (_a) {
// ignore
}
};
this.closingPromise = (this.activeBlockingCommands > 0
? Promise.resolve().then(closeRaw)
: this.operationChain
.catch(() => {
// ignore
})
.then(async () => {
if (!this.closed) {
return;
}
if (this.raw) {
await this.clearConnectionNameIfNeeded(this.raw);
}
closeRaw();
})).finally(() => {
this.closingPromise = undefined;
});
}
return this.closingPromise;
}
disconnect(reconnect = false) {
if (this.closed && !reconnect) {
return;
}
this.closed = true;
this.readyEmitted = false;
this.statusOverride = reconnect ? undefined : 'end';
this.emit('close');
if (reconnect) {
void this.closeRaw()
.then(() => {
this.emit('reconnecting');
return this.connect();
})
.catch(error => this.emit('error', error));
return;
}
void this.closeRaw();
this.emit('end');
}
async quit() {
if (this.closed) {
setImmediate(() => {
this.emit('end');
this.emit('close');
});
return 'OK';
}
this.closed = true;
this.readyEmitted = false;
this.statusOverride = 'end';
void this.closeRaw();
setImmediate(() => {
this.emit('end');
this.emit('close');
});
return 'OK';
}
duplicate(...args) {
var _a;
const options = ((_a = args[0]) !== null && _a !== void 0 ? _a : {});
const duplicated = (async () => {
var _a, _b;
const raw = await this.ensureRaw();
const clientConstructor = raw.constructor;
const createClient = (_a = clientConstructor === null || clientConstructor === void 0 ? void 0 : clientConstructor.createClient) === null || _a === void 0 ? void 0 : _a.bind(clientConstructor);
const config = (_b = raw.config) !== null && _b !== void 0 ? _b : raw.options;
if (!createClient || !config) {
throw new Error('BullMQ: Cannot duplicate Valkey Glide client: missing createClient() or ' +
'config. Ensure the client was created via GlideClient.createClient()/' +
'GlideClusterClient.createClient().');
}
return createClient(config);
})();
return new ValkeyGlideAdapter(duplicated, options.connectionName);
}
defineCommand(name, definition) {
const sha = (0, crypto_1.createHash)('sha1').update(definition.lua).digest('hex');
const script = {
sha,
lua: definition.lua,
numberOfKeys: definition.numberOfKeys,
};
this.scripts.set(name, script);
this.scriptsBySha.set(sha, script);
this[name] = (...args) => this.runCommand(name, args);
void this.ensureScriptLoaded(script);
}
async runCommand(name, args) {
const script = this.scripts.get(name);
if (!script) {
throw new Error(`BullMQ: command "${name}" is not defined. Use defineCommand() before runCommand().`);
}
const normalizedArgs = normalizeScriptArgs(args);
const keys = normalizedArgs.slice(0, script.numberOfKeys).map(toGlideArg);
const argv = normalizedArgs.slice(script.numberOfKeys).map(toGlideArg);
const evalShaArgs = [
'EVALSHA',
script.sha,
String(script.numberOfKeys),
...keys,
...argv,
];
try {
return await this.runRawCommand(evalShaArgs);
}
catch (err) {
if (typeof (err === null || err === void 0 ? void 0 : err.message) === 'string' &&
err.message.toLowerCase().includes('noscript')) {
return this.runRawCommand([
'EVAL',
script.lua,
String(script.numberOfKeys),
...keys,
...argv,
]);
}
throw err;
}
}
multi() {
return new ValkeyGlideTransaction(this, this.scripts);
}
pipeline() {
return this.multi();
}
async hgetall(key) {
return normalizeHashReply(await this.runRawCommand(['HGETALL', key]));
}
async hget(key, field) {
const value = await this.runRawCommand(['HGET', key, field]);
return value == null ? null : toStringValue(value);
}
async hmget(key, ...fields) {
const value = await this.runRawCommand(['HMGET', key, ...fields]);
return Array.isArray(value)
? value.map(item => (item == null ? null : toStringValue(item)))
: [];
}
async hset(key, data) {
const args = ['HSET', key];
for (const [field, value] of Object.entries(data)) {
args.push(field, toGlideArg(value));
}
return Number(await this.runRawCommand(args));
}
async hdel(key, ...fields) {
return Number(await this.runRawCommand(['HDEL', key, ...fields]));
}
async hexists(key, field) {
const result = await this.runRawCommand(['HEXISTS', key, field]);
if (typeof result === 'boolean') {
return result ? 1 : 0;
}
return Number(result);
}
async get(key) {
const value = await this.runRawCommand(['GET', key]);
return value == null ? null : toStringValue(value);
}
async set(key, value, options) {
const args = ['SET', key, toGlideArg(value)];
if ((options === null || options === void 0 ? void 0 : options.PX) != null) {
args.push('PX', String(options.PX));
}
else if ((options === null || options === void 0 ? void 0 : options.EX) != null) {
args.push('EX', String(options.EX));
}
const result = await this.runRawCommand(args);
return result == null ? null : toStringValue(result);
}
async del(...keys) {
if (keys.length === 0) {
return 0;
}
return Number(await this.runRawCommand(['DEL', ...keys]));
}
async zrange(key, start, end, options) {
const args = ['ZRANGE', key, String(start), String(end)];
if (options === null || options === void 0 ? void 0 : options.WITHSCORES) {
args.push('WITHSCORES');
}
const result = await this.runRawCommand(args);
return normalizeSortedSetReply(result, options === null || options === void 0 ? void 0 : options.WITHSCORES);
}
async zrevrange(key, start, end, options) {
const args = ['ZREVRANGE', key, String(start), String(end)];
if (options === null || options === void 0 ? void 0 : options.WITHSCORES) {
args.push('WITHSCORES');
}
const result = await this.runRawCommand(args);
return normalizeSortedSetReply(result, options === null || options === void 0 ? void 0 : options.WITHSCORES);
}
async zcard(key) {
return Number(await this.runRawCommand(['ZCARD', key]));
}
async zscore(key, member) {
const result = await this.runRawCommand(['ZSCORE', key, member]);
return result == null ? null : toStringValue(result);
}
async lrange(key, start, end) {
const result = await this.runRawCommand([
'LRANGE',
key,
String(start),
String(end),
]);
return Array.isArray(result) ? result.map(toStringValue) : [];
}
async llen(key) {
return Number(await this.runRawCommand(['LLEN', key]));
}
async ltrim(key, start, end) {
const result = await this.runRawCommand([
'LTRIM',
key,
String(start),
String(end),
]);
return result == null ? 'OK' : toStringValue(result);
}
async lpos(key, value) {
const result = await this.runRawCommand(['LPOS', key, value]);
return result == null ? null : Number(result);
}
async smembers(key) {
const result = await this.runRawCommand(['SMEMBERS', key]);
return Array.isArray(result) ? result.map(toStringValue) : [];
}
async xadd(key, id, fields, options) {
const args = ['XADD', key];
if ((options === null || options === void 0 ? void 0 : options.MAXLEN) != null) {
args.push('MAXLEN');
if (options.approximate !== false) {
args.push('~');
}
args.push(String(options.MAXLEN));
}
args.push(id);
for (const [field, value] of Object.entries(fields)) {
args.push(field, toGlideArg(value));
}
return toStringValue(await this.runRawCommand(args));
}
async xread(streams, options) {
const args = ['XREAD'];
if ((options === null || options === void 0 ? void 0 : options.BLOCK) != null) {
args.push('BLOCK', String(options.BLOCK));
}
if ((options === null || options === void 0 ? void 0 : options.COUNT) != null) {
args.push('COUNT', String(options.COUNT));
}
args.push('STREAMS');
for (const stream of streams) {
args.push(stream.key);
}
for (const stream of streams) {
args.push(stream.id);
}
return normalizeXReadReply(await this.runRawCommand(args));
}
async xtrim(key, strategy, threshold, options) {
const args = ['XTRIM', key, strategy];
if ((options === null || options === void 0 ? void 0 : options.approximate) !== false) {
args.push('~');
}
args.push(String(threshold));
return Number(await this.runRawCommand(args));
}
async bzpopmin(key, timeout) {
const result = await this.runRawCommand(['BZPOPMIN', key, String(timeout)]);
if (!result) {
return null;
}
if (Array.isArray(result) && result.length >= 3) {
return [
toStringValue(result[0]),
toStringValue(result[1]),
toStringValue(result[2]),
];
}
return null;
}
async info() {
return toStringValue(await this.runRawCommand(['INFO']));
}
async clientSetName(name) {
return this.runRawCommand(['CLIENT', 'SETNAME', name], {
decoder: GLIDE_STRING_DECODER,
});
}
async clientList() {
return toStringValue(await this.runRawCommand(['CLIENT', 'LIST'], {
decoder: GLIDE_STRING_DECODER,
}));
}
async scan(cursor, options) {
const args = ['SCAN', String(cursor)];
if (options === null || options === void 0 ? void 0 : options.MATCH) {
args.push('MATCH', options.MATCH);
}
if ((options === null || options === void 0 ? void 0 : options.COUNT) != null) {
args.push('COUNT', String(options.COUNT));
}
return normalizeScanReply(await this.runRawCommand(args));
}
scanStream(options) {
let cursor = '0';
let running = false;
const stream = new stream_1.Readable({
objectMode: true,
read: () => {
if (running) {
return;
}
running = true;
(async () => {
do {
const [nextCursor, keys] = await this.scan(cursor, {
MATCH: options.match,
COUNT: options.count,
});
cursor = nextCursor;
if (keys.length > 0 && !stream.push(keys)) {
return;
}
} while (cursor !== '0');
stream.push(null);
})()
.catch(err => stream.destroy(err))
.finally(() => {
running = false;
});
},
});
return stream;
}
async keys(pattern) {
const result = await this.runRawCommand(['KEYS', pattern]);
return Array.isArray(result) ? result.map(toStringValue) : [];
}
async exists(...keys) {
if (keys.length === 0) {
return 0;
}
const result = await this.runRawCommand(['EXISTS', ...keys]);
if (typeof result === 'boolean') {
return result ? 1 : 0;
}
return Number(result);
}
async zadd(key, ...args) {
const commandArgs = ['ZADD', key];
for (let i = 0; i < args.length; i += 2) {
commandArgs.push(toGlideArg(args[i]), toGlideArg(args[i + 1]));
}
return Number(await this.runRawCommand(commandArgs));
}
async zrem(key, ...members) {
return Number(await this.runRawCommand(['ZREM', key, ...members]));
}
async xlen(key) {
return Number(await this.runRawCommand(['XLEN', key]));
}
async xrevrange(key, end, start, ...rest) {
const args = ['XREVRANGE', key, end, start];
if (rest[0] === 'COUNT') {
args.push('COUNT', toGlideArg(rest[1]));
}
return normalizeXRangeReply(await this.runRawCommand(args));
}
async sadd(key, ...members) {
return Number(await this.runRawCommand([
'SADD',
key,
...members.map(member => toGlideArg(member)),
]));
}
async scard(key) {
return Number(await this.runRawCommand(['SCARD', key]));
}
async lpush(key, ...values) {
return Number(await this.runRawCommand(['LPUSH', key, ...values]));
}
async rpop(key) {
const result = await this.runRawCommand(['RPOP', key]);
return result == null ? null : toStringValue(result);
}
async incr(key) {
return Number(await this.runRawCommand(['INCR', key]));
}
async incrby(key, increment) {
return Number(await this.runRawCommand(['INCRBY', key, String(increment)]));
}
async flushall() {
const result = await this.runRawCommand(['FLUSHALL']);
return result == null ? 'OK' : toStringValue(result);
}
async execQueuedCommands(commands) {
const requiredScripts = commands
.map(command => String(command.args[0]).toUpperCase() === 'EVALSHA'
? this.scriptsBySha.get(toStringValue(command.args[1]))
: undefined)
.filter((script) => Boolean(script));
if (requiredScripts.length > 0) {
await Promise.all(requiredScripts.map(script => this.ensureScriptLoaded(script)));
}
return this.runSerialized(async (raw) => {
if (typeof raw.exec === 'function') {
const nativeBatch = createGlideBatch(true);
const batch = nativeBatch !== null && nativeBatch !== void 0 ? nativeBatch : createCompatibilityBatch();
if (batch) {
for (const command of commands) {
batch.customCommand(command.args);
}
let execResult;
try {
execResult = await raw.exec(batch, false);
}
catch (err) {
if (nativeBatch) {
throw err;
}
execResult = null;
}
if (!execResult) {
if (nativeBatch) {
return null;
}
}
else {
return execResult.map((value, index) => {
var _a;
if (value instanceof Error) {
return [value, null];
}
const transform = (_a = commands[index]) === null || _a === void 0 ? void 0 : _a.transform;
return [null, transform ? transform(value) : value];
});
}
}
}
await raw.customCommand(['MULTI']);
try {
for (const command of commands) {
// During MULTI queueing Redis replies with "QUEUED". For commands such
// as HGETALL, Glide's default decoder expects a structured reply and
// throws if it receives this simple string, so force string decoding.
await raw.customCommand(command.args, {
decoder: GLIDE_STRING_DECODER,
});
}
const execResult = await raw.customCommand(['EXEC']);
if (!execResult) {
return null;
}
const results = Array.isArray(execResult) ? execResult : [execResult];
return results.map((value, index) => {
var _a;
if (value instanceof Error) {
return [value, null];
}
const transform = (_a = commands[index]) === null || _a === void 0 ? void 0 : _a.transform;
return [null, transform ? transform(value) : value];
});
}
catch (err) {
try {
await raw.customCommand(['DISCARD']);
}
catch (_a) {
// ignore
}
throw err;
}
});
}
}
class ValkeyGlideTransaction {
constructor(adapter, scripts) {
this.adapter = adapter;
this.scripts = scripts;
this.commands = [];
}
queueCommand(args, transform) {
this.commands.push({ args, transform });
return this;
}
hgetall(key) {
return this.queueCommand(['HGETALL', key], normalizeHashReply);
}
hset(key, data) {
const args = ['HSET', key];
for (const [field, value] of Object.entries(data)) {
args.push(field, toGlideArg(value));
}
return this.queueCommand(args);
}
hscan(key, cursor, options) {
const args = ['HSCAN', key, String(cursor)];
if ((options === null || options === void 0 ? void 0 : options.COUNT) != null) {
args.push('COUNT', String(options.COUNT));
}
return this.queueCommand(args, value => {
if (Array.isArray(value) && value.length >= 2) {
return [toStringValue(value[0]), flattenFieldPairs(value[1])];
}
return ['0', []];
});
}
smembers(key) {
return this.queueCommand(['SMEMBERS', key], value => Array.isArray(value) ? value.map(item => toStringValue(item)) : []);
}
sscan(key, cursor, options) {
const args = ['SSCAN', key, String(cursor)];
if ((options === null || options === void 0 ? void 0 : options.COUNT) != null) {
args.push('COUNT', String(options.COUNT));
}
return this.queueCommand(args, value => {
if (Array.isArray(value) && value.length >= 2) {
const members = Array.isArray(value[1])
? value[1].map(item => toStringValue(item))
: [];
return [toStringValue(value[0]), members];
}
return ['0', []];
});
}
zrange(key, start, end) {
return this.queueCommand(['ZRANGE', key, String(start), String(end)]);
}
lrange(key, start, end) {
return this.queueCommand(['LRANGE', key, String(start), String(end)]);
}
llen(key) {
return this.queueCommand(['LLEN', key]);
}
del(...keys) {
if (keys.length > 0) {
this.queueCommand(['DEL', ...keys]);
}
return this;
}
runCommand(name, args) {
const script = this.scripts.get(name);
if (!script) {
throw new Error(`BullMQ: command "${name}" is not defined. Use defineCommand() before adding it to transactions.`);
}
const normalizedArgs = normalizeScriptArgs(args);
const keys = normalizedArgs.slice(0, script.numberOfKeys).map(toGlideArg);
const argv = normalizedArgs.slice(script.numberOfKeys).map(toGlideArg);
return this.queueCommand([
'EVALSHA',
script.sha,
String(script.numberOfKeys),
...keys,
...argv,
]);
}
exec() {
return this.adapter.execQueuedCommands(this.commands);
}
}

892
node_modules/bullmq/dist/cjs/classes/worker.js generated vendored Normal file
View File

@@ -0,0 +1,892 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Worker = void 0;
const fs = require("fs");
const url_1 = require("url");
const path = require("path");
const abort_controller_1 = require("./abort-controller");
const utils_1 = require("../utils");
const queue_base_1 = require("./queue-base");
const child_pool_1 = require("./child-pool");
const sandbox_1 = require("./sandbox");
const async_fifo_queue_1 = require("./async-fifo-queue");
const errors_1 = require("./errors");
const enums_1 = require("../enums");
const job_scheduler_1 = require("./job-scheduler");
const lock_manager_1 = require("./lock-manager");
// 10 seconds is the maximum time a BZPOPMIN can block.
const maximumBlockTimeout = 10;
/**
*
* This class represents a worker that is able to process jobs from the queue.
* As soon as the class is instantiated and a connection to Redis is established
* it will start processing jobs.
*
*/
class Worker extends queue_base_1.QueueBase {
static RateLimitError() {
return new errors_1.RateLimitError();
}
constructor(name, processor, opts, backendFactory) {
super(name, Object.assign(Object.assign({ drainDelay: 5, concurrency: 1, lockDuration: 30000, maximumRateLimitDelay: 30000, maxStalledCount: 1, stalledInterval: 30000, autorun: true, runRetryDelay: 15000 }, opts), { blockingConnection: true }), backendFactory);
this.abortDelayController = null;
this.blockUntil = 0;
this.drained = false;
this.limitUntil = 0;
this.processorAcceptsSignal = false;
this.stalledCheckerRunning = false;
this.waiting = null;
this.running = false;
this.mainLoopRunning = null;
if (!opts || !opts.connection) {
throw new Error('Worker requires a connection');
}
if (typeof this.opts.maxStalledCount !== 'number' ||
this.opts.maxStalledCount < 0) {
throw new Error('maxStalledCount must be greater or equal than 0');
}
if (typeof this.opts.maxStartedAttempts === 'number' &&
this.opts.maxStartedAttempts < 0) {
throw new Error('maxStartedAttempts must be greater or equal than 0');
}
if (typeof this.opts.stalledInterval !== 'number' ||
this.opts.stalledInterval <= 0) {
throw new Error('stalledInterval must be greater than 0');
}
if (typeof this.opts.drainDelay !== 'number' || this.opts.drainDelay <= 0) {
throw new Error('drainDelay must be greater than 0');
}
this.concurrency = this.opts.concurrency;
this.opts.lockRenewTime =
this.opts.lockRenewTime || this.opts.lockDuration / 2;
this.id = (0, utils_1.randomUUID)();
this.createLockManager();
if (processor) {
if (typeof processor === 'function') {
this.processFn = processor;
// Check if processor accepts signal parameter (3rd parameter)
this.processorAcceptsSignal = processor.length >= 3;
}
else {
// SANDBOXED
if (processor instanceof url_1.URL) {
if (!fs.existsSync(processor)) {
throw new Error(`URL ${processor} does not exist in the local file system`);
}
processor = processor.href;
}
else {
const supportedFileTypes = ['.js', '.ts', '.flow', '.cjs', '.mjs'];
const processorFile = processor +
(supportedFileTypes.includes(path.extname(processor)) ? '' : '.js');
if (!fs.existsSync(processorFile)) {
throw new Error(`File ${processorFile} does not exist`);
}
}
// Separate paths so that bundling tools can resolve dependencies easier
const dirname = path.dirname(module.filename || __filename);
const workerThreadsMainFile = path.join(dirname, 'main-worker.js');
const spawnProcessMainFile = path.join(dirname, 'main.js');
let mainFilePath = this.opts.useWorkerThreads
? workerThreadsMainFile
: spawnProcessMainFile;
try {
fs.statSync(mainFilePath); // would throw if file not exists
}
catch (_) {
const mainFile = this.opts.useWorkerThreads
? 'main-worker.js'
: 'main.js';
mainFilePath = path.join(process.cwd(), `dist/cjs/classes/${mainFile}`);
fs.statSync(mainFilePath);
}
this.childPool = new child_pool_1.ChildPool({
mainFile: mainFilePath,
useWorkerThreads: this.opts.useWorkerThreads,
workerForkOptions: this.opts.workerForkOptions,
workerThreadsOptions: this.opts.workerThreadsOptions,
});
this.createSandbox(processor);
this.processorAcceptsSignal = true;
}
if (this.opts.autorun) {
this.run().catch(error => this.emit('error', error));
}
}
// The Worker is only "ready" once its dedicated blocking connection (used
// by the blocking `waitForJob` primitive) is ready. The backend forwards a
// 'ready' event from *either* of its connections, so we cannot rely on that
// event to gate on blocking readiness. Instead emit a single 'ready' once
// `backend.waitUntilReady()` resolves, which only happens after both the
// main and the blocking connections are ready. Connection failures are
// surfaced via the 'error' event wired up in QueueBase.
this.backend
.waitUntilReady()
.then(() => setTimeout(() => this.emit('ready'), 0))
.catch(() => { });
}
/**
* Builds the worker's backend, which owns both the regular connection and a
* dedicated blocking connection used by the `waitForJob` primitive.
*/
createBackend() {
this.backend = this.backendFactory(this.name, this.opts, {
withBlockingConnection: true,
});
}
/**
* Creates and configures the lock manager for processing jobs.
* This method can be overridden in subclasses to customize lock manager behavior.
*/
createLockManager() {
this.lockManager = new lock_manager_1.LockManager(this, {
lockRenewTime: this.opts.lockRenewTime,
lockDuration: this.opts.lockDuration,
workerId: this.id,
workerName: this.opts.name,
});
}
/**
* Creates and configures the sandbox for processing jobs.
* This method can be overridden in subclasses to customize sandbox behavior.
*
* @param processor - The processor file path, URL, or function to be sandboxed
*/
createSandbox(processor) {
this.processFn = (0, sandbox_1.default)(processor, this.childPool).bind(this);
}
/**
* Public accessor method for LockManager to extend locks.
* This delegates to the protected scripts object.
*/
async extendJobLocks(jobIds, tokens, duration) {
return this.backend.extendLocks(jobIds, tokens, duration);
}
emit(event, ...args) {
return super.emit(event, ...args);
}
off(eventName, listener) {
super.off(eventName, listener);
return this;
}
on(event, listener) {
super.on(event, listener);
return this;
}
once(event, listener) {
super.once(event, listener);
return this;
}
callProcessJob(job, token, signal) {
return this.processFn(job, token, signal);
}
createJob(data, jobId) {
return this.Job.fromJSON(this, data, jobId);
}
/**
*
* Waits until the worker is ready to start processing jobs.
* In general only useful when writing tests.
*
*/
async waitUntilReady() {
await super.waitUntilReady();
}
/**
* Cancels a specific job currently being processed by this worker.
* The job's processor function will receive an abort signal.
*
* @param jobId - The ID of the job to cancel
* @param reason - Optional reason for the cancellation
* @returns true if the job was found and cancelled, false otherwise
*/
cancelJob(jobId, reason) {
return this.lockManager.cancelJob(jobId, reason);
}
/**
* Cancels all jobs currently being processed by this worker.
* All active job processor functions will receive abort signals.
*
* @param reason - Optional reason for the cancellation
*/
cancelAllJobs(reason) {
this.lockManager.cancelAllJobs(reason);
}
set concurrency(concurrency) {
if (typeof concurrency !== 'number' ||
concurrency < 1 ||
!isFinite(concurrency)) {
throw new Error('concurrency must be a finite number greater than 0');
}
this._concurrency = concurrency;
}
get concurrency() {
return this._concurrency;
}
get jobScheduler() {
return new Promise(async (resolve) => {
if (!this._jobScheduler) {
// Share the worker's backend (same queue) with the scheduler.
this._jobScheduler = new job_scheduler_1.JobScheduler(this.name, this.opts, () => this.backend);
this._jobScheduler.on('error', this.emit.bind(this, 'error'));
}
resolve(this._jobScheduler);
});
}
async run() {
if (!this.processFn) {
throw new Error('No process function is defined.');
}
if (this.running) {
throw new Error('Worker is already running.');
}
try {
this.running = true;
if (this.closing || this.paused) {
return;
}
await this.startStalledCheckTimer();
if (!this.opts.skipLockRenewal) {
this.lockManager.start();
}
this.mainLoopRunning = this.mainLoop();
// We must await here or finally will be called too early.
await this.mainLoopRunning;
}
finally {
this.running = false;
}
}
async waitForRateLimit() {
var _a;
const limitUntil = this.limitUntil;
if (limitUntil > Date.now()) {
(_a = this.abortDelayController) === null || _a === void 0 ? void 0 : _a.abort();
this.abortDelayController = new abort_controller_1.AbortController();
const delay = this.getRateLimitDelay(limitUntil - Date.now());
await this.delay(delay, this.abortDelayController);
this.drained = false;
this.limitUntil = 0;
}
}
/**
* This is the main loop in BullMQ. Its goals are to fetch jobs from the queue
* as efficiently as possible, providing concurrency and minimal unnecessary calls
* to Redis.
*/
async mainLoop() {
const asyncFifoQueue = new async_fifo_queue_1.AsyncFifoQueue();
let tokenPostfix = 0;
while ((!this.closing && !this.paused) || asyncFifoQueue.numTotal() > 0) {
/**
* This inner loop tries to fetch jobs concurrently, but if we are waiting for a job
* to arrive at the queue we should not try to fetch more jobs (as it would be pointless)
*/
while (!this.closing &&
!this.paused &&
!this.waiting &&
asyncFifoQueue.numTotal() < this._concurrency &&
!this.isRateLimited()) {
const token = `${this.id}:${tokenPostfix++}`;
const fetchedJob = this.retryIfFailed(() => this._getNextJob(token, { block: true }), {
delayInMs: this.opts.runRetryDelay,
onlyEmitError: true,
});
asyncFifoQueue.add(fetchedJob);
if (this.waiting && asyncFifoQueue.numTotal() > 1) {
// We are waiting for jobs but we have others that we could start processing already
break;
}
// We await here so that we fetch jobs in sequence, this is important to avoid unnecessary calls
// to Redis in high concurrency scenarios.
const job = await fetchedJob;
// No more jobs waiting but we have others that could start processing already
if (!job && asyncFifoQueue.numTotal() > 1) {
break;
}
// If there are potential jobs to be processed and blockUntil is set, we should exit to avoid waiting
// for processing this job.
if (this.blockUntil) {
break;
}
}
// Since there can be undefined jobs in the queue (when a job fails or queue is empty)
// we iterate until we find a job.
let job;
do {
job = await asyncFifoQueue.fetch();
} while (!job && asyncFifoQueue.numQueued() > 0);
if (job) {
const token = job.token;
asyncFifoQueue.add(this.processJob(job, token, () => asyncFifoQueue.numTotal() <= this._concurrency));
}
else if (asyncFifoQueue.numQueued() === 0) {
await this.waitForRateLimit();
}
}
}
/**
* Returns a promise that resolves to the next job in queue.
* @param token - worker token to be assigned to retrieved job
* @returns a Job or undefined if no job was available in the queue.
*/
async getNextJob(token, { block = true } = {}) {
var _a, _b;
const nextJob = await this._getNextJob(token, { block });
return this.trace(enums_1.SpanKind.INTERNAL, 'getNextJob', this.name, async (span) => {
span === null || span === void 0 ? void 0 : span.setAttributes({
[enums_1.TelemetryAttributes.WorkerId]: this.id,
[enums_1.TelemetryAttributes.QueueName]: this.name,
[enums_1.TelemetryAttributes.WorkerName]: this.opts.name,
[enums_1.TelemetryAttributes.WorkerOptions]: JSON.stringify({ block }),
[enums_1.TelemetryAttributes.JobId]: nextJob === null || nextJob === void 0 ? void 0 : nextJob.id,
});
return nextJob;
}, (_b = (_a = nextJob === null || nextJob === void 0 ? void 0 : nextJob.opts) === null || _a === void 0 ? void 0 : _a.telemetry) === null || _b === void 0 ? void 0 : _b.metadata);
}
async _getNextJob(token, { block = true } = {}) {
if (this.paused) {
return;
}
if (this.closing) {
return;
}
let job;
if (this.drained && block && !this.limitUntil && !this.waiting) {
this.waiting = this.waitForJob(this.blockUntil);
try {
this.blockUntil = await this.waiting;
if (this.blockUntil <= 0 || this.blockUntil - Date.now() < 1) {
job = await this.moveToActive(token, this.opts.name);
}
}
finally {
this.waiting = null;
}
}
else {
if (!this.isRateLimited()) {
job = await this.moveToActive(token, this.opts.name);
}
}
return job;
}
/**
* Overrides the rate limit to be active for the next jobs.
* @deprecated This method is deprecated and will be removed in v6. Use queue.rateLimit method instead.
* @param expireTimeMs - expire time in ms of this rate limit.
*/
async rateLimit(expireTimeMs) {
await this.trace(enums_1.SpanKind.INTERNAL, 'rateLimit', this.name, async (span) => {
span === null || span === void 0 ? void 0 : span.setAttributes({
[enums_1.TelemetryAttributes.WorkerId]: this.id,
[enums_1.TelemetryAttributes.WorkerRateLimit]: expireTimeMs,
});
await this.backend.setRateLimit(expireTimeMs);
});
}
get minimumBlockTimeout() {
return this.backend.minimumBlockTimeout;
}
isRateLimited() {
return this.limitUntil > Date.now();
}
async moveToActive(token, name) {
const [jobData, id, rateLimitDelay, delayUntil] = await this.backend.moveToActive(token, name);
this.updateDelays(rateLimitDelay, delayUntil);
return this.nextJobFromJobData(jobData, id, token);
}
async waitForJob(blockUntil) {
if (this.paused) {
return Infinity;
}
try {
if (!this.closing && !this.isRateLimited()) {
const blockTimeout = this.getBlockTimeout(blockUntil);
if (blockTimeout > 0) {
this.updateDelays(); // reset delays to avoid reusing same values in next iteration
// Markers should only be used for un-blocking, so we will handle them
// in this function only. Capability-based timeout rounding and the
// stuck-connection watchdog are owned by the backend's waitForJob.
const result = await this.backend.waitForJob(blockTimeout);
if (result) {
const newBlockUntil = result.score;
// Use by pro version as rate limited groups could generate lower blockUntil values
// markers only return delays for delayed jobs
if (blockUntil && newBlockUntil > blockUntil) {
return blockUntil;
}
return newBlockUntil;
}
}
return 0;
}
}
catch (error) {
if ((0, utils_1.isNotConnectionError)(error)) {
this.emit('error', error);
}
// The watchdog must abort every overdue blocking command, but doing so
// during socketless Sentinel resolution can leave the blocking transport
// disconnected. Ask the backend to recover it before retrying.
if (!this.closing) {
try {
await this.backend.reconnectBlocking();
}
catch (reconnectError) {
if ((0, utils_1.isNotConnectionError)(reconnectError)) {
this.emit('error', reconnectError);
}
}
}
if (!this.closing) {
await this.delay();
}
}
return Infinity;
}
getBlockTimeout(blockUntil) {
const opts = this.opts;
// when there are delayed jobs
if (blockUntil) {
const blockDelay = blockUntil - Date.now();
// when we reach the time to get new jobs
if (blockDelay <= 0) {
return blockDelay;
}
else if (blockDelay < this.minimumBlockTimeout * 1000) {
return this.minimumBlockTimeout;
}
else {
// We restrict the maximum block timeout to 10 second to avoid
// blocking the connection for too long in the case of reconnections
// reference: https://github.com/taskforcesh/bullmq/issues/1658
return Math.min(blockDelay / 1000, maximumBlockTimeout);
}
}
else {
return Math.max(opts.drainDelay, this.minimumBlockTimeout);
}
}
getRateLimitDelay(delay) {
// We restrict the maximum limit delay to the configured maximumRateLimitDelay
// to be able to promote delayed jobs while the queue is rate limited
return Math.min(delay, this.opts.maximumRateLimitDelay);
}
/**
*
* This function is exposed only for testing purposes.
*/
async delay(milliseconds, abortController) {
await (0, utils_1.delay)(milliseconds || utils_1.DELAY_TIME_1, abortController);
}
updateDelays(limitDelay = 0, delayUntil = 0) {
const clampedLimit = Math.max(limitDelay, 0);
if (clampedLimit > 0) {
this.limitUntil = Date.now() + clampedLimit;
}
else {
this.limitUntil = 0;
}
this.blockUntil = Math.max(delayUntil, 0) || 0;
}
async nextJobFromJobData(jobData, jobId, token) {
if (!jobData) {
if (!this.drained) {
this.emit('drained');
this.drained = true;
}
}
else {
this.drained = false;
const job = this.createJob(jobData, jobId);
job.token = token;
try {
const shouldScheduleRepeat = await this.retryIfFailed(async () => {
// We need to distinguish between new job schedulers and legacy
// repeatable jobs. We first use a key-shape heuristic to detect
// legacy repeat keys (`name:id:endDate:tz:<cron|every>`), then
// probe the per-id scheduler metadata hash (`repeat:<id>` with
// the `ic` field) via `JobScheduler.isJobScheduler()` for
// disambiguation when needed (see issue #3828).
const hasRepeatJobKey = !!job.repeatJobKey;
const hasLegacyKeyShape = hasRepeatJobKey && (0, job_scheduler_1.hasLegacyRepeatableKeyShape)(job.repeatJobKey);
let isJobScheduler = hasRepeatJobKey && !hasLegacyKeyShape;
if (hasLegacyKeyShape) {
const jobScheduler = await this.jobScheduler;
isJobScheduler = await jobScheduler.isJobScheduler(job.repeatJobKey);
}
if (isJobScheduler) {
const jobScheduler = await this.jobScheduler;
await jobScheduler.upsertJobScheduler(
// Most of these arguments are not really needed
// anymore as we read them from the job scheduler itself
job.repeatJobKey, job.opts.repeat, job.name, job.data, job.opts, { override: false, producerId: job.id });
}
return !hasLegacyKeyShape || isJobScheduler;
}, { delayInMs: this.opts.runRetryDelay });
if (job.repeatJobKey && !shouldScheduleRepeat) {
const schedulingError = new Error(`Failed to add repeatable job for next iteration: ${(0, job_scheduler_1.getLegacyRepeatableJobError)(job.repeatJobKey).message}`);
this.emit('error', schedulingError);
}
}
catch (err) {
// Emit error but don't throw to avoid breaking current job completion
// Note: This means the next repeatable job will not be scheduled
const errorMessage = err instanceof Error ? err.message : String(err);
const schedulingError = new Error(`Failed to add repeatable job for next iteration: ${errorMessage}`);
this.emit('error', schedulingError);
// Return undefined to indicate no next job is available
return undefined;
}
this.emit('active', job, 'waiting');
return job;
}
}
async processJob(job, token, fetchNextCallback = () => true) {
var _a, _b;
const srcPropagationMetadata = (_b = (_a = job.opts) === null || _a === void 0 ? void 0 : _a.telemetry) === null || _b === void 0 ? void 0 : _b.metadata;
return this.trace(enums_1.SpanKind.CONSUMER, 'process', this.name, async (span) => {
span === null || span === void 0 ? void 0 : span.setAttributes({
[enums_1.TelemetryAttributes.WorkerId]: this.id,
[enums_1.TelemetryAttributes.WorkerName]: this.opts.name,
[enums_1.TelemetryAttributes.JobId]: job.id,
[enums_1.TelemetryAttributes.JobName]: job.name,
});
const abortController = this.lockManager.trackJob(job.id, token, job.processedOn, this.processorAcceptsSignal);
try {
const unrecoverableErrorMessage = this.getUnrecoverableErrorMessage(job);
if (unrecoverableErrorMessage) {
const failed = await this.retryIfFailed(() => {
this.lockManager.untrackJob(job.id);
return this.handleFailed(new errors_1.UnrecoverableError(unrecoverableErrorMessage), job, token, fetchNextCallback, span);
}, { delayInMs: this.opts.runRetryDelay, span });
return failed;
}
const result = await this.callProcessJob(job, token, abortController
? abortController.signal
: undefined);
return await this.retryIfFailed(() => {
this.lockManager.untrackJob(job.id);
return this.handleCompleted(result, job, token, fetchNextCallback, span);
}, { delayInMs: this.opts.runRetryDelay, span });
}
catch (err) {
const failed = await this.retryIfFailed(() => {
this.lockManager.untrackJob(job.id);
return this.handleFailed(err, job, token, fetchNextCallback, span);
}, { delayInMs: this.opts.runRetryDelay, span, onlyEmitError: true });
return failed;
}
finally {
this.lockManager.untrackJob(job.id);
const now = Date.now();
span === null || span === void 0 ? void 0 : span.setAttributes({
[enums_1.TelemetryAttributes.JobAttemptFinishedTimestamp]: job.finishedOn || now,
[enums_1.TelemetryAttributes.JobProcessedTimestamp]: job.processedOn,
});
}
}, srcPropagationMetadata);
}
getUnrecoverableErrorMessage(job) {
if (job.deferredFailure) {
return job.deferredFailure;
}
if (this.opts.maxStartedAttempts &&
this.opts.maxStartedAttempts < job.attemptsStarted) {
return 'job started more than allowable limit';
}
}
async handleCompleted(result, job, token, fetchNextCallback = () => true, span) {
if (!this.backend.closing) {
const completed = await job.moveToCompleted(result, token, fetchNextCallback() && !(this.closing || this.paused));
this.emit('completed', job, result, 'active');
span === null || span === void 0 ? void 0 : span.addEvent('job completed', {
[enums_1.TelemetryAttributes.JobResult]: JSON.stringify(result),
});
span === null || span === void 0 ? void 0 : span.setAttributes({
[enums_1.TelemetryAttributes.JobAttemptsMade]: job.attemptsMade,
});
if (Array.isArray(completed)) {
const [jobData, jobId, rateLimitDelay, delayUntil] = completed;
this.updateDelays(rateLimitDelay, delayUntil);
return this.nextJobFromJobData(jobData, jobId, token);
}
}
}
async handleFailed(err, job, token, fetchNextCallback = () => true, span) {
if (!this.backend.closing) {
// Check if the job was manually rate-limited
if (err.message === errors_1.RATE_LIMIT_ERROR) {
const rateLimitTtl = await this.moveLimitedBackToWait(job, token);
this.limitUntil = rateLimitTtl > 0 ? Date.now() + rateLimitTtl : 0;
return;
}
const fetchNext = fetchNextCallback() && !(this.closing || this.paused);
if (err instanceof errors_1.DelayedError ||
err.name == 'DelayedError' ||
err instanceof errors_1.WaitingError ||
err.name == 'WaitingError' ||
err instanceof errors_1.WaitingChildrenError ||
err.name == 'WaitingChildrenError') {
if (!fetchNext) {
return;
}
return this.moveToActive(token, this.opts.name);
}
const result = await job.moveToFailed(err, token, fetchNext);
this.emit('failed', job, err, 'active');
span === null || span === void 0 ? void 0 : span.addEvent('job failed', {
[enums_1.TelemetryAttributes.JobFailedReason]: err.message,
});
span === null || span === void 0 ? void 0 : span.setAttributes({
[enums_1.TelemetryAttributes.JobAttemptsMade]: job.attemptsMade,
});
// Note: result can be undefined if moveToFailed fails (e.g., lock was lost)
if (Array.isArray(result)) {
const [jobData, jobId, rateLimitDelay, delayUntil] = result;
this.updateDelays(rateLimitDelay, delayUntil);
return this.nextJobFromJobData(jobData, jobId, token);
}
}
}
/**
*
* Pauses the processing of this queue only for this worker.
*/
async pause(doNotWaitActive) {
await this.trace(enums_1.SpanKind.INTERNAL, 'pause', this.name, async (span) => {
var _a;
span === null || span === void 0 ? void 0 : span.setAttributes({
[enums_1.TelemetryAttributes.WorkerId]: this.id,
[enums_1.TelemetryAttributes.WorkerName]: this.opts.name,
[enums_1.TelemetryAttributes.WorkerDoNotWaitActive]: doNotWaitActive,
});
if (!this.paused) {
this.paused = true;
if (!doNotWaitActive) {
await this.whenCurrentJobsFinished();
}
(_a = this.stalledCheckStopper) === null || _a === void 0 ? void 0 : _a.call(this);
this.emit('paused');
}
});
}
/**
*
* Resumes processing of this worker (if paused).
*/
async resume() {
try {
if (!this.running || this.paused) {
await this.trace(enums_1.SpanKind.INTERNAL, 'resume', this.name, async (span) => {
span === null || span === void 0 ? void 0 : span.setAttributes({
[enums_1.TelemetryAttributes.WorkerId]: this.id,
[enums_1.TelemetryAttributes.WorkerName]: this.opts.name,
});
this.paused = false;
if (!this.running) {
if (this.processFn) {
this.run();
}
}
else {
// Main loop is still running (pause was called with doNotWaitActive=true).
// Restart the stalled checker since pause() stopped it.
await this.startStalledCheckTimer();
}
this.emit('resumed');
});
}
}
catch (error) {
this.emit('error', error);
}
}
/**
*
* Checks if worker is paused.
*
* @returns true if worker is paused, false otherwise.
*/
isPaused() {
return !!this.paused;
}
/**
*
* Checks if worker is currently running.
*
* @returns true if worker is running, false otherwise.
*/
isRunning() {
return this.running;
}
/**
*
* Closes the worker and related redis connections.
*
* This method waits for current jobs to finalize before returning.
*
* @param force - Use force boolean parameter if you do not want to wait for
* current jobs to be processed. When using telemetry, be mindful that it can
* interfere with the proper closure of spans, potentially preventing them from being exported.
*
* @returns Promise that resolves when the worker has been closed.
*/
async close(force = false) {
if (this.closing) {
return this.closing;
}
this.closing = (async () => {
await this.trace(enums_1.SpanKind.INTERNAL, 'close', this.name, async (span) => {
var _a, _b;
span === null || span === void 0 ? void 0 : span.setAttributes({
[enums_1.TelemetryAttributes.WorkerId]: this.id,
[enums_1.TelemetryAttributes.WorkerName]: this.opts.name,
[enums_1.TelemetryAttributes.WorkerForceClose]: force,
});
this.emit('closing', 'closing queue');
(_a = this.abortDelayController) === null || _a === void 0 ? void 0 : _a.abort();
// Define the async cleanup functions
const asyncCleanups = [
() => {
return force || this.whenCurrentJobsFinished(false);
},
() => this.lockManager.close(),
() => { var _a; return (_a = this.childPool) === null || _a === void 0 ? void 0 : _a.clean(); },
() => this.backend.close(force),
];
// Run cleanup functions sequentially and make sure all are run despite any errors
for (const cleanup of asyncCleanups) {
try {
await cleanup();
}
catch (err) {
this.emit('error', err);
}
}
(_b = this.stalledCheckStopper) === null || _b === void 0 ? void 0 : _b.call(this);
this.closed = true;
this.emit('closed');
});
})();
return await this.closing;
}
/**
*
* Manually starts the stalled checker.
* The check will run once as soon as this method is called, and
* then every opts.stalledInterval milliseconds until the worker is closed.
* Note: Normally you do not need to call this method, since the stalled checker
* is automatically started when the worker starts processing jobs after
* calling run. However if you want to process the jobs manually you need
* to call this method to start the stalled checker.
*
* @see {@link https://docs.bullmq.io/patterns/manually-fetching-jobs}
*/
async startStalledCheckTimer() {
if (!this.opts.skipStalledCheck) {
if (!this.closing && !this.stalledCheckerRunning) {
await this.trace(enums_1.SpanKind.INTERNAL, 'startStalledCheckTimer', this.name, async (span) => {
span === null || span === void 0 ? void 0 : span.setAttributes({
[enums_1.TelemetryAttributes.WorkerId]: this.id,
[enums_1.TelemetryAttributes.WorkerName]: this.opts.name,
});
this.stalledCheckerRunning = true;
this.stalledChecker()
.catch(err => {
this.emit('error', err);
})
.finally(() => {
this.stalledCheckerRunning = false;
});
});
}
}
}
async stalledChecker() {
while (!(this.closing || this.paused)) {
await this.checkConnectionError(() => this.moveStalledJobsToWait());
await new Promise(resolve => {
const timeout = setTimeout(resolve, this.opts.stalledInterval);
this.stalledCheckStopper = () => {
clearTimeout(timeout);
resolve();
};
});
}
}
/**
* Returns a promise that resolves when active jobs are cleared
*
* @returns
*/
async whenCurrentJobsFinished(reconnect = true) {
// The blocking connection is dedicated to bzpopmin, so it is safe to
// always disconnect it whenever the main loop is running. Waiting for the
// actual disconnect ('end' event) is required to avoid a race where the
// bzpopmin call is still in flight when the main loop awaits its result.
if (this.mainLoopRunning) {
await this.backend.disconnectBlocking(true);
await this.mainLoopRunning;
}
else {
reconnect = false;
}
reconnect && (await this.backend.reconnectBlocking());
}
async retryIfFailed(fn, opts) {
var _a;
let retry = 0;
const maxRetries = opts.maxRetries || Infinity;
do {
try {
return await fn();
}
catch (err) {
(_a = opts.span) === null || _a === void 0 ? void 0 : _a.recordException(err.message);
if ((0, utils_1.isNotConnectionError)(err)) {
// Emit error when not paused or closing; optionally swallow (no throw) when opts.onlyEmitError is set.
if (!this.paused && !this.closing) {
this.emit('error', err);
}
if (opts.onlyEmitError) {
return;
}
else {
throw err;
}
}
else {
if (opts.delayInMs && !this.closing && !this.closed) {
await this.delay(opts.delayInMs, this.abortDelayController);
}
if (retry + 1 >= maxRetries) {
// If we've reached max retries, throw the last error
throw err;
}
}
}
} while (++retry < maxRetries);
}
async moveStalledJobsToWait() {
await this.trace(enums_1.SpanKind.INTERNAL, 'moveStalledJobsToWait', this.name, async (span) => {
const stalled = await this.backend.moveStalledJobsToWait();
span === null || span === void 0 ? void 0 : span.setAttributes({
[enums_1.TelemetryAttributes.WorkerId]: this.id,
[enums_1.TelemetryAttributes.WorkerName]: this.opts.name,
[enums_1.TelemetryAttributes.WorkerStalledJobs]: stalled,
});
stalled.forEach((jobId) => {
span === null || span === void 0 ? void 0 : span.addEvent('job stalled', {
[enums_1.TelemetryAttributes.JobId]: jobId,
});
this.emit('stalled', jobId, 'active');
});
});
}
moveLimitedBackToWait(job, token) {
return job.moveToWait(token);
}
}
exports.Worker = Worker;