dt-common-device
Version:
A secure and robust device management library for IoT applications
193 lines (192 loc) • 7.21 kB
JavaScript
;
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");
}