@penkov/tasks_queue
Version:
A lightweight PostgreSQL-backed task queue system with scheduling, retries, backoff strategies, and priority handling. Designed for efficiency and observability in modern Node.js applications.
84 lines (83 loc) • 3.45 kB
JavaScript
import { MultiStepTask } from "./multi-step-task.js";
import { option } from "scats";
export class ManagedWorkflowTask extends MultiStepTask {
maxRuns;
constructor(maxRuns) {
super();
this.maxRuns = ManagedWorkflowTask.validateMaxRuns(option(maxRuns).getOrElseValue(100));
}
static validateMaxRuns(value) {
const normalized = Number(value);
if (!Number.isFinite(normalized) ||
normalized <= 0 ||
!Number.isInteger(normalized)) {
throw new Error("Managed workflow maxRuns must be a positive integer");
}
return normalized;
}
readCurrentRunCount(payload) {
const rawValue = option(payload.workflowPayload.runCount).orUndefined;
if (rawValue === undefined) {
return 0;
}
const parsed = Number(rawValue);
if (!Number.isFinite(parsed) || parsed < 0 || !Number.isInteger(parsed)) {
throw new Error(`Invalid workflow runCount value: ${String(rawValue)}. runCount must be a non-negative integer`);
}
return parsed;
}
async processNext(payload, context) {
const currentRunCount = this.readCurrentRunCount(payload);
const nextRunCount = currentRunCount + 1;
if (nextRunCount > this.maxRuns) {
throw new Error(`Workflow exceeded maximum parent runs: ${nextRunCount} > ${this.maxRuns}`);
}
const nextPayload = payload.copy({
workflowPayload: {
...payload.workflowPayload,
runCount: nextRunCount,
},
});
context.setPayload(nextPayload.toJson);
let childSpawned = false;
let resultSubmitted = false;
const managedContext = {
taskId: context.taskId,
currentAttempt: context.currentAttempt,
maxAttempts: context.maxAttempts,
resolvedChildTask: context.resolvedChildTask,
ping: () => context.ping(),
findTask: (taskId) => context.findTask(taskId),
setPayload: (nextUserPayload) => {
const wrapped = nextPayload.copy({
userPayload: nextUserPayload,
});
context.setPayload(wrapped.toJson);
},
spawnChild: (task) => {
if (childSpawned) {
throw new Error("Managed workflow cannot spawn more than one child in a single run");
}
if (resultSubmitted) {
throw new Error("Managed workflow cannot spawn a child after submitting result");
}
childSpawned = true;
context.spawnChild(task);
},
submitResult: (result) => {
if (resultSubmitted) {
throw new Error("Managed workflow cannot submit more than one result in a single run");
}
if (childSpawned) {
throw new Error("Managed workflow cannot submit result after spawning a child");
}
resultSubmitted = true;
context.submitResult(result);
},
};
await this.run(nextPayload.userPayload, managedContext);
if (!childSpawned && !resultSubmitted) {
throw new Error("Managed workflow must either spawn a child task or submit a result in each run");
}
}
}