UNPKG

@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
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); } }