@sotatech/nest-taskflow
Version:
A task flow management library for NestJS with Redis Pub/Sub integration.
239 lines • 10.3 kB
JavaScript
;
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