UNPKG

dt-common-device

Version:

A secure and robust device management library for IoT applications

193 lines (192 loc) 7.21 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.enqueueEvent = enqueueEvent; exports.startConsumer = startConsumer; exports.closeNotificationQueue = closeNotificationQueue; const bullmq_1 = require("bullmq"); const config_1 = require("../config/config"); const ioredis_1 = __importDefault(require("ioredis")); const QUEUE_NAME = "notification-events"; // Singleton instances let notificationQueue = null; let notificationWorker = null; let queueEvents = null; let redisConnection = null; // Track active jobs per userId to maintain FIFO order per user // while allowing parallel processing across different users const activeJobsPerUser = new Map(); /** * Get or create Redis connection for BullMQ */ function getRedisConnection() { if (!redisConnection) { const { host, port } = (0, config_1.getRedisDbHostAndPort)(); redisConnection = new ioredis_1.default({ host, port, maxRetriesPerRequest: null, enableReadyCheck: false, }); redisConnection.on("error", (error) => { (0, config_1.getConfig)().LOGGER.error("Redis connection error for BullMQ", { error }); }); redisConnection.on("connect", () => { (0, config_1.getConfig)().LOGGER.info("Redis connected for BullMQ"); }); } return redisConnection; } /** * Get or create the notification queue instance */ function getQueue() { if (!notificationQueue) { const connection = getRedisConnection(); notificationQueue = new bullmq_1.Queue(QUEUE_NAME, { connection, defaultJobOptions: { backoff: { type: "exponential", delay: 2000, }, removeOnComplete: { age: 5 * 60 }, // Keep completed jobs for 5 minutes removeOnFail: { age: 5 * 60 }, // Remove failed jobs after 5 minutes attempts: 1, // Only try once }, }); // Set up queue event listeners for monitoring queueEvents = new bullmq_1.QueueEvents(QUEUE_NAME, { connection }); queueEvents.on("completed", ({ jobId }) => { (0, config_1.getConfig)().LOGGER.info(`Notification job ${jobId} completed`); }); queueEvents.on("failed", ({ jobId, failedReason }) => { (0, config_1.getConfig)().LOGGER.error(`Notification job ${jobId} failed`, { failedReason, }); }); } return notificationQueue; } /** * Enqueue an event payload to BullMQ queue for guaranteed delivery. * Jobs are processed in parallel across different userIds but maintain FIFO order per userId. * This replaces the previous Redis pub/sub implementation. */ async function enqueueEvent(event) { const queue = getQueue(); try { // Use userId as part of jobId to ensure proper ordering per user await queue.add("notification-event", event, { jobId: `${event.userId}-${Date.now()}-${Math.random() .toString(36) .substr(2, 9)}`, }); (0, config_1.getConfig)().LOGGER.info("Notification event enqueued successfully", { userId: event.userId, }); } catch (error) { (0, config_1.getConfig)().LOGGER.error("Failed to enqueue notification event", { error, event, }); throw error; } } /** * Start a BullMQ worker to process notification events. * Jobs are processed in parallel across different userIds but maintain FIFO order per userId. * The `handler` is responsible for business logic (e.g. calling processEvent()). * This replaces the previous Redis pub/sub consumer implementation. */ async function startConsumer(handler) { if (notificationWorker) { return; } const connection = getRedisConnection(); try { notificationWorker = new bullmq_1.Worker(QUEUE_NAME, async (job) => { const payload = job.data; const userId = payload.userId; // Wait for any existing job for this userId to complete (FIFO per user) const previousJob = activeJobsPerUser.get(userId); if (previousJob) { try { await previousJob; } catch (error) { // Previous job failed, but we still want to process this one (0, config_1.getConfig)().LOGGER.warn(`Previous job for userId ${userId} failed, continuing with next job`, { error }); } } // Create the promise for this job const jobPromise = (async () => { try { await handler(payload); } catch (error) { // Re-throw to let BullMQ handle retries throw error; } finally { // Remove from active jobs when done // Since only one job per userId runs at a time, we can safely delete by userId activeJobsPerUser.delete(userId); } })(); // Store the promise for this userId activeJobsPerUser.set(userId, jobPromise); // Wait for this job to complete await jobPromise; }, { connection, removeOnComplete: { age: 5 * 60 }, // Remove after 5 minutes removeOnFail: { age: 5 * 60 }, // Remove failed jobs after 5 minutes }); notificationWorker.on("completed", (job) => { (0, config_1.getConfig)().LOGGER.info(`Notification job ${job.id} completed`); }); notificationWorker.on("failed", (job, err) => { (0, config_1.getConfig)().LOGGER.error(`Notification job ${job?.id} failed`, { error: err, attemptsMade: job?.attemptsMade, }); }); notificationWorker.on("error", (error) => { (0, config_1.getConfig)().LOGGER.error("Notification worker error", { error }); }); } catch (error) { (0, config_1.getConfig)().LOGGER.error("Failed to start notification worker", { error, }); throw error; } } /** * Gracefully close the worker and queue connections */ async function closeNotificationQueue() { const promises = []; if (notificationWorker) { promises.push(notificationWorker.close()); notificationWorker = null; } if (notificationQueue) { promises.push(notificationQueue.close()); notificationQueue = null; } if (queueEvents) { promises.push(queueEvents.close()); queueEvents = null; } if (redisConnection) { promises.push(redisConnection.quit().then(() => undefined)); redisConnection = null; } await Promise.all(promises); (0, config_1.getConfig)().LOGGER.info("Notification queue connections closed"); }