@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.
134 lines (133 loc) • 5.14 kB
JavaScript
import { none, option } from "scats";
import { MultiStepTask } from "./multi-step-task.js";
export class SequentialTask extends MultiStepTask {
steps;
constructor(steps) {
super();
this.steps = steps;
}
nextStep(currentStep) {
const currentIdx = this.steps.indexOf(currentStep);
if (currentIdx < 0) {
throw new Error(`Unknown sequential step: ${currentStep}`);
}
return option(currentIdx + 1)
.filter((idx) => idx < this.steps.length)
.map((idx) => this.steps.get(idx));
}
async continueToNextStep(payload, context) {
await this.currentStep(payload).match({
some: async (currentStep) => {
await this.nextStep(currentStep).match({
some: async (step) => {
const nextPayload = payload.copy({
workflowPayload: {
...payload.workflowPayload,
step,
},
});
context.setPayload(nextPayload.toJson);
await this.runStep(nextPayload, step, context);
},
none: async () => {
},
});
},
none: async () => {
throw new Error("Sequential task requires at least one configured step");
},
});
}
processStepContext(payload, context, step, trackers) {
return {
taskId: context.taskId,
currentAttempt: context.currentAttempt,
maxAttempts: context.maxAttempts,
resolvedChildTask: context.resolvedChildTask,
ping: () => context.ping(),
setPayload: (userPayload) => {
const nextPayload = payload.copy({
workflowPayload: {
...payload.workflowPayload,
step,
},
userPayload: userPayload,
});
option(trackers)
.flatMap((t) => option(t.onSetPayload))
.foreach((cb) => cb(nextPayload));
context.setPayload(nextPayload.toJson);
},
submitResult: (result) => context.submitResult(result),
findTask: (taskId) => context.findTask(taskId),
spawnChild: (task) => {
option(trackers)
.flatMap((t) => option(t.onSpawnChild))
.foreach((cb) => cb());
context.spawnChild(task);
},
};
}
clearedResolvedChildContext(context) {
return {
taskId: context.taskId,
currentAttempt: context.currentAttempt,
maxAttempts: context.maxAttempts,
resolvedChildTask: none,
ping: () => context.ping(),
setPayload: (payload) => context.setPayload(payload),
submitResult: (result) => context.submitResult(result),
findTask: (taskId) => context.findTask(taskId),
spawnChild: (task) => context.spawnChild(task),
};
}
async runStep(payload, step, context) {
let effectivePayload = payload;
let childSpawned = false;
await this.processStep(step, payload.userPayload, this.processStepContext(payload, context, step, {
onSetPayload: (nextPayload) => {
effectivePayload = nextPayload;
},
onSpawnChild: () => {
childSpawned = true;
},
}));
if (!childSpawned) {
await this.continueToNextStep(effectivePayload, this.clearedResolvedChildContext(context));
}
}
async processNext(payload, context) {
await this.currentStep(payload).match({
some: async (step) => {
if (payload.workflowPayload["step"] !== step) {
context.setPayload(payload.copy({
workflowPayload: {
...payload.workflowPayload,
step,
},
}).toJson);
}
await this.runStep(payload, step, context);
},
none: async () => {
throw new Error("Sequential task requires at least one configured step");
},
});
}
async childFinished(payload, _childTask, context, _activeChild) {
await this.continueToNextStep(payload, context);
}
async childFailed(payload, childTask, context, activeChild) {
if (activeChild.allowFailure) {
await this.continueToNextStep(payload, context);
return;
}
await super.childFailed(payload, childTask, context, activeChild);
}
stepFromPayload(payload) {
return option(payload.workflowPayload["step"]).map((x) => x);
}
currentStep(payload) {
return this.stepFromPayload(payload).orElseValue(this.steps.headOption);
}
}