UNPKG

torotask

Version:

Task queue processing in NodeJS based on BullMQ and Redis

168 lines 8.74 kB
import { UnrecoverableError } from 'bullmq'; import * as z from 'zod'; import { BaseTask } from './base-task.js'; import { SubTask } from './sub-task.js'; import { StepExecutor } from './step-executor.js'; import { isControlError } from './utils/is-control-error.js'; /** * Represents a defined task associated with a TaskGroup. * Extends BaseTask to manage its own underlying BullMQ queue and worker. * Implements the `process` method by calling the specific task `handler` or a registered subtask handler. * Can define and manage SubTasks. * * @template PayloadExplicit The explicitly provided payload type. Defaults to `unknown`. * If not `unknown`, this type takes precedence. * If `unknown`, type is inferred from `Schema` or defaults to `any`. * @template ResultType The expected return type of the job. Defaults to `unknown`. * @template SchemaInputVal The input schema type for payload validation and type inference. Defaults to `undefined`. */ export class Task extends BaseTask { /** * A type-only property to help infer the actual payload type of the task. * This is equivalent to EffectivePayloadType<PayloadExplicit, ResolvedSchemaType<SchemaInputVal>>. */ _payloadType; /** * A type-only property to help infer the result type of the task. */ _resultType; subTasks; // SubTask payload/result types might need further consideration for inference allowCatchAll; // The handler's payload type is determined by EffectivePayloadType. // TaskHandler (imported) uses EffectivePayloadType from types/task.ts. // The local EffectivePayloadType must match for consistency. handler; schema; // Stores the RESOLVED schema instance or undefined constructor(taskGroup, id, // config's PayloadExplicit defaults to unknown, ResultType to unknown, SchemaInputVal to undefined // TaskDefinition itself uses EffectivePayloadType internally for its handler and triggers. config, groupLogger) { // Pass the effective payload type to BaseTask's triggers if necessary, // currently TaskDefinition handles the trigger's payload type. super(taskGroup, id, config.options ?? {}, config.triggers, groupLogger); this.handler = config.handler; // config.schema is now directly SchemaInputVal (which is ZodSchema | undefined) // ResolvedSchemaType<SchemaInputVal> will correctly resolve this. // So, this.schema (which is ResolvedSchemaType<SchemaInputVal>) can be directly assigned. this.schema = config.schema; this.subTasks = new Map(); this.allowCatchAll = this.options.allowCatchAll ?? true; this.logger.debug({ allowCatchAll: this.allowCatchAll, triggerCount: this.triggers?.length ?? 0, // Access triggers from config or this.triggers if super sets it hasPayloadSchema: !!this.schema, }, 'Task initialized'); } defineSubTask(subTaskId, subTaskHandler) { if (this.subTasks.has(subTaskId)) { this.logger.warn({ subTaskId }, 'SubTask already defined. Overwriting.'); } if (subTaskId === this.id) { this.logger.error({ subTaskId }, 'SubTask id cannot be the same as the parent Task id.'); throw new Error(`SubTask id "${subTaskId}" cannot be the same as the parent Task id "${this.id}".`); } // SubTask's payload type defaults to the parent's effective payload type const newSubTask = new SubTask(this, subTaskId, subTaskHandler); this.subTasks.set(subTaskId, newSubTask); this.logger.debug({ subTaskId }, 'SubTask defined'); return newSubTask; } getSubTask(subTaskId) { return this.subTasks.get(subTaskId); } async process(job, token) { const { id, name: jobName } = job; const effectiveJobName = jobName === '' || jobName === '__default__' ? this.id : jobName; const jobLogger = this.logger.child({ jobId: id, jobName: effectiveJobName }); const subTask = this.subTasks.get(effectiveJobName); try { if (subTask) { jobLogger.info(`Routing job to SubTask handler: ${effectiveJobName}`); // SubTask processing might also need payload validation if schemas are defined there // For now, assumes subtask handles its own payload typing/validation if necessary const result = await subTask.processSubJob(job, effectiveJobName, jobLogger); // Cast job if subtask has different payload type jobLogger.info(`SubTask job completed successfully`); return result; } else if (effectiveJobName === this.id || this.allowCatchAll) { // Pass jobLogger to processJob const result = await this.processJob(job, token, jobLogger); jobLogger.debug(`Main task job completed successfully`); return result; } else { jobLogger.error(`Job name "${effectiveJobName}" does not match main task or subtasks, and catchAll is disabled.`); throw new Error(`Job name "${effectiveJobName}" on queue "${this.queueName}" not recognized by task "${this.id}".`); } } catch (error) { // Check for workflow pending errors using instanceof first (type-safe) and then by name/property (more flexible) if (isControlError(error)) { jobLogger.debug({ err: error, name: error.name, errorType: error.constructor.name, stepId: error?.stepInternalId, }, `Job processing resulted in a workflow pending state (${error.name}). Re-throwing for BullMQ to handle.`); // Always re-throw workflow pending errors so BullMQ can handle them throw error; } jobLogger.error({ err: error instanceof Error ? error : new Error(String(error)) }, `Job processing failed for job name "${effectiveJobName}"`); throw error; } } async validateJob(job, jobLogger) { const effectiveJobLogger = jobLogger ?? this.getJobLogger(job); effectiveJobLogger.debug('Validating job payload'); let result = job.payload; // For non-batch jobs, no schema is defined or validation is skipped, return the payload as is if (job.isBatch || !this.schema || this.options.skipSchemaValidation) { return result; } try { // Type assertion needed because this.schema is Schema (ZodSchema | undefined) // and parse expects `this` to be ZodSchema<CurrentPayloadType> result = this.schema.parse(job.payload); effectiveJobLogger.debug('Payload validated successfully.'); } catch (error) { effectiveJobLogger.error({ err: error instanceof z.ZodError ? error.format() : error, originalPayload: job.payload, }, `Payload validation failed for job "${job.name}" (id: ${job.id})`); if (error instanceof z.ZodError) { const messages = error.errors.map((e) => `${e.path.join('.') || 'payload'}: ${e.message}`); throw new UnrecoverableError(`Payload validation failed: ${messages.join('; ')}`); } throw error; } job.payload = result; return result; } async processJob(job, token, jobLogger) { const effectiveJobLogger = jobLogger ?? this.getJobLogger(job); // Use CurrentPayloadType which is derived from the local EffectivePayloadType const validatedPayload = await this.validateJob(job, effectiveJobLogger); const handlerOptions = { id: job.id, name: this.id, payload: validatedPayload, }; const stepExecutor = new StepExecutor(job, this); // TaskHandlerContext's PayloadType will be CurrentPayloadType const handlerContext = { logger: effectiveJobLogger, client: this.taskClient, group: this.group, task: this, // `this` is Task<PayloadExplicit, ResultType, SchemaInputVal> // Context expects Task<CurrentPayloadType, ResultType, SchemaInputVal> // This cast is generally safe due to how CurrentPayloadType is derived. job: job, step: stepExecutor, token, queue: this.queue, }; return await this.handler(handlerOptions, handlerContext); } } //# sourceMappingURL=task.js.map