UNPKG

@sotatech/nest-taskflow

Version:

A task flow management library for NestJS with Redis Pub/Sub integration.

239 lines 10.3 kB
"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; var __param = (this && this.__param) || function (paramIndex, decorator) { return function (target, key) { decorator(target, key, paramIndex); } }; var TaskFlowService_1; Object.defineProperty(exports, "__esModule", { value: true }); exports.TaskFlowService = void 0; const common_1 = require("@nestjs/common"); const nestjs_redis_1 = require("@liaoliaots/nestjs-redis"); const constants_1 = require("./constants"); const enums_1 = require("./enums"); const strategies_1 = require("./strategies"); const uuid_1 = require("uuid"); let TaskFlowService = TaskFlowService_1 = class TaskFlowService { constructor(moduleOptions, redisService, strategyRegistry) { this.moduleOptions = moduleOptions; this.redisService = redisService; this.strategyRegistry = strategyRegistry; this.logger = new common_1.Logger(TaskFlowService_1.name); this.redisClient = this.redisService.getOrThrow('client'); } async addTask(queueName, data, options) { try { const taskId = this.generateTaskId(); const taskMetadata = this.createTaskMetadata(taskId, queueName, data, options); await this.saveTaskMetadata(taskId, taskMetadata, options.ttl); await this.executeStrategies(taskId, taskMetadata, options); await this.addToQueue(queueName, taskId, taskMetadata.priority); this.logger.log(`Task ${taskId} added to queue ${queueName}`); return taskMetadata; } catch (error) { return Promise.reject(`Failed to add task to queue ${queueName}: ${error}`); } } async verify(taskId, method, otp) { try { const otpKey = `otp:${taskId}:${method}`; const savedOtp = await this.redisClient.get(otpKey); if (!savedOtp || savedOtp !== otp) { throw new Error(`Invalid OTP for task ${taskId}`); } const [, metadata] = await Promise.all([ this.redisClient.del(otpKey), this.getTaskMetadata(taskId), ]); await this.updateTaskStatus(taskId, enums_1.TaskFlowStatus.SUCCESS, method); await this.redisClient.publish('task_verified', JSON.stringify(metadata)); this.logger.log(`Task ${taskId} verified via ${method}`); return metadata; } catch { return Promise.reject(`Failed to verify session ${taskId} via ${method}. OTP wrong or expired.`); } } async resendOtp(taskId, method) { try { const metadata = await this.getTaskMetadata(taskId); if (!this.isValidOtpRequest(metadata, method)) { return; } const strategy = this.strategyRegistry.get(method); const otp = await this.generateAndSendOtp(strategy, metadata); await this.cacheOtp(taskId, method, otp, metadata); } catch (error) { return Promise.reject(`OTP resend failed for task ${taskId}: ${error}`); } } isValidOtpRequest(metadata, method) { if (!metadata.recipient || !method) { this.logger.warn('Missing recipient or method'); return false; } if (!this.strategyRegistry.get(method)) { this.logger.warn(`Invalid OTP method: ${method}`); return false; } return true; } async generateAndSendOtp(strategy, metadata) { const otp = await strategy.generate({ ...metadata.data, recipient: metadata.recipient, }); await strategy.send(metadata, otp); return otp; } async cacheOtp(taskId, method, otp, metadata) { const ttl = metadata.ttl || this.moduleOptions.jobTimeout || 30000; await this.redisClient.set(`otp:${taskId}:${method}`, otp, 'EX', Math.ceil(ttl / 1000)); } async updateRecipient(taskId, newRecipient) { try { const metadata = await this.getTaskMetadata(taskId); metadata.recipient = newRecipient; await this.persistUpdatedRecipient(taskId, newRecipient); await this.resendOtpsForAllMethods(taskId); } catch (error) { return Promise.reject(`Failed to update recipient for task ${taskId}: ${error}`); } } async persistUpdatedRecipient(taskId, newRecipient) { await this.redisClient.hmset(`task:${taskId}`, { recipient: JSON.stringify(newRecipient), }); } async resendOtpsForAllMethods(taskId) { await this.cleanupOtpKeys(taskId); const strateries = Array.from(this.strategyRegistry.getAll().keys()); const otpPromises = Object.keys(strateries).map((method) => this.resendOtp(taskId, method)); await Promise.all(otpPromises); } createTaskMetadata(taskId, queueName, data, options) { return { id: taskId, queue: queueName, data: data, recipient: options.recipient, status: enums_1.TaskFlowStatus.PENDING, priority: options.priority || 0, timeout: options.timeout || this.moduleOptions.jobTimeout || 30000, timestamp: Date.now(), ttl: options.ttl, }; } async saveTaskMetadata(taskId, metadata, ttl) { const serializedMetadata = { ...metadata, data: JSON.stringify(metadata.data), recipient: JSON.stringify(metadata.recipient), ttl: metadata.ttl ? metadata.ttl : -1, }; await this.redisClient.hmset(`task:${taskId}`, serializedMetadata); if (ttl) { await this.redisClient.expire(`task:${taskId}`, Math.ceil(ttl / 1000)); } } async executeStrategies(taskId, metadata, options) { for (const method of options.allowedMethods) { const strategy = this.strategyRegistry.get(method); if (!strategy) { this.logger.warn(`No strategy found for method: ${method}`); continue; } const otp = await strategy.generate(metadata); await strategy.send(metadata, otp); await this.redisClient.set(`otp:${taskId}:${method}`, otp, 'EX', Math.ceil(options.ttl || 30000 / 1000)); } } async addToQueue(queueName, taskId, priority) { const queueKey = priority > 0 ? `priority_queue:${queueName}` : `queue:${queueName}`; const queueMethod = priority > 0 ? 'zadd' : 'rpush'; await this.redisClient[queueMethod](queueKey, priority, taskId); } async updateTaskStatus(taskId, status, method) { const taskKey = `task:${taskId}`; const taskData = await this.redisClient.hgetall(taskKey); if (!taskData) { this.logger.warn(`Task with ID ${taskId} not found in Redis`); return; } const queueKey = parseInt(taskData.priority) > 0 ? `priority_queue:${taskData.queue}` : `queue:${taskData.queue}`; if (status === enums_1.TaskFlowStatus.SUCCESS) { this.logger.log(`Task ${taskId} successfully verified. Cleaning up.`); await Promise.all([ this.redisClient.del(taskKey), this.removeFromQueue(queueKey, taskId), this.cleanupOtpKeys(taskId), ]); } else { await this.redisClient.hmset(taskKey, { status, verifiedAt: Date.now(), verificationMethod: method, }); } } async removeFromQueue(queueKey, taskId) { const queueType = await this.redisClient.type(queueKey); const removalMap = { zset: () => this.redisClient.zrem(queueKey, taskId), list: () => this.redisClient.lrem(queueKey, 0, taskId), }; const removalFn = removalMap[queueType]; if (removalFn) { await removalFn(); } else { this.logger.warn(`Queue key ${queueKey} has unexpected type: ${queueType}`); } } async cleanupOtpKeys(taskId) { const otpCleanupKeys = Object.values(enums_1.TaskFlowMethods).map((method) => `otp:${taskId}:${method}`); await Promise.all(otpCleanupKeys.map((key) => this.redisClient.del(key))); } generateTaskId() { return (0, uuid_1.v4)(); } async getTaskMetadata(taskId) { const metadata = await this.redisClient.hgetall(`task:${taskId}`); if (!metadata || Object.keys(metadata).length === 0) { common_1.Logger.error(`Task with ID ${taskId} not found`); throw new Error(`Task with ID ${taskId} not found`); } return { ...metadata, data: JSON.parse(metadata.data || '{}'), recipient: JSON.parse(metadata.recipient || '{}'), priority: Number(metadata.priority), timeout: Number(metadata.timeout), timestamp: Number(metadata.timestamp), }; } onModuleDestroy() { this.redisClient.disconnect(); } }; exports.TaskFlowService = TaskFlowService; exports.TaskFlowService = TaskFlowService = TaskFlowService_1 = __decorate([ (0, common_1.Injectable)(), __param(0, (0, common_1.Inject)(constants_1.TASKFLOW_OPTIONS)), __metadata("design:paramtypes", [Object, nestjs_redis_1.RedisService, strategies_1.StrategyRegistry]) ], TaskFlowService); //# sourceMappingURL=taskflow.service.js.map