dt-common-device 13.0.16 → 13.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,37 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
37
  };
@@ -308,5 +341,13 @@ async function shutdown() {
308
341
  getConfig().LOGGER.error("Failed to stop event subscription", { error });
309
342
  }
310
343
  }
344
+ // Close notification queue connections
345
+ try {
346
+ const { closeNotificationQueue } = await Promise.resolve().then(() => __importStar(require("../notificationQueue/queue.service")));
347
+ await closeNotificationQueue();
348
+ }
349
+ catch (error) {
350
+ getConfig().LOGGER.error("Failed to close notification queue", { error });
351
+ }
311
352
  getConfig().LOGGER.info("dt-common-device: Shutdown completed");
312
353
  }
package/dist/index.js CHANGED
@@ -65,5 +65,5 @@ __exportStar(require("./webhookQueue"), exports);
65
65
  __exportStar(require("./copilotQueue"), exports);
66
66
  //Export Service Queue
67
67
  __exportStar(require("./serviceQueue"), exports);
68
- // Export Notification Queue (Redis pub/sub for notifications)
68
+ // Export Notification Queue (BullMQ for guaranteed delivery)
69
69
  __exportStar(require("./notificationQueue"), exports);
@@ -1 +1,2 @@
1
1
  export * from "./queue.service";
2
+ export * from "./interfaces";
@@ -15,3 +15,4 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./queue.service"), exports);
18
+ __exportStar(require("./interfaces"), exports);
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Payload structure for notification queue events
3
+ */
4
+ export interface INotificationQueuePayload {
5
+ userId: string;
6
+ orgId?: string;
7
+ propertyId?: string;
8
+ data?: any;
9
+ title?: string;
10
+ message?: string;
11
+ url?: string;
12
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1 @@
1
+ export * from "./INotificationQueue";
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./INotificationQueue"), exports);
@@ -1,21 +1,18 @@
1
- export interface NotificationQueuePayload {
2
- userId: string;
3
- orgId?: string;
4
- propertyId?: string;
5
- data?: any;
6
- title?: string;
7
- message?: string;
8
- url?: string;
9
- }
10
- export interface NotificationQueueOptions {
11
- channel?: string;
12
- }
1
+ import { INotificationQueuePayload } from "./interfaces";
13
2
  /**
14
- * Publish an event payload to Redis so any interested consumer can process it.
3
+ * Enqueue an event payload to BullMQ queue for guaranteed delivery.
4
+ * Jobs are processed in parallel across different userIds but maintain FIFO order per userId.
5
+ * This replaces the previous Redis pub/sub implementation.
15
6
  */
16
- export declare function enqueueEvent(event: NotificationQueuePayload, opts?: NotificationQueueOptions): Promise<void>;
7
+ export declare function enqueueEvent(event: INotificationQueuePayload): Promise<void>;
17
8
  /**
18
- * Start a single pub/sub consumer in this process.
9
+ * Start a BullMQ worker to process notification events.
10
+ * Jobs are processed in parallel across different userIds but maintain FIFO order per userId.
19
11
  * The `handler` is responsible for business logic (e.g. calling processEvent()).
12
+ * This replaces the previous Redis pub/sub consumer implementation.
20
13
  */
21
- export declare function startConsumer(handler: (payload: NotificationQueuePayload) => Promise<void>, opts?: NotificationQueueOptions): Promise<void>;
14
+ export declare function startConsumer(handler: (payload: INotificationQueuePayload) => Promise<void>): Promise<void>;
15
+ /**
16
+ * Gracefully close the worker and queue connections
17
+ */
18
+ export declare function closeNotificationQueue(): Promise<void>;
@@ -1,96 +1,192 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.enqueueEvent = enqueueEvent;
4
7
  exports.startConsumer = startConsumer;
5
- const redis_1 = require("../db/redis");
6
- const DEFAULT_CHANNEL = "notification:events";
7
- let consumerStarted = false;
8
+ exports.closeNotificationQueue = closeNotificationQueue;
9
+ const bullmq_1 = require("bullmq");
10
+ const config_1 = require("../config/config");
11
+ const ioredis_1 = __importDefault(require("ioredis"));
12
+ const QUEUE_NAME = "notification:events";
13
+ // Singleton instances
14
+ let notificationQueue = null;
15
+ let notificationWorker = null;
16
+ let queueEvents = null;
17
+ let redisConnection = null;
18
+ // Track active jobs per userId to maintain FIFO order per user
19
+ // while allowing parallel processing across different users
20
+ const activeJobsPerUser = new Map();
8
21
  /**
9
- * Publish an event payload to Redis so any interested consumer can process it.
22
+ * Get or create Redis connection for BullMQ
10
23
  */
11
- async function enqueueEvent(event, opts) {
12
- const client = (0, redis_1.getRedisClient)();
13
- const channel = opts?.channel ?? DEFAULT_CHANNEL;
14
- try {
15
- await client.publish(channel, JSON.stringify(event));
16
- }
17
- catch (error) {
18
- // eslint-disable-next-line no-console
19
- console.error("Failed to publish notification event", {
20
- error,
21
- channel,
22
- event,
24
+ function getRedisConnection() {
25
+ if (!redisConnection) {
26
+ const { host, port } = (0, config_1.getRedisDbHostAndPort)();
27
+ redisConnection = new ioredis_1.default({
28
+ host,
29
+ port,
30
+ maxRetriesPerRequest: null,
31
+ enableReadyCheck: false,
32
+ });
33
+ redisConnection.on("error", (error) => {
34
+ (0, config_1.getConfig)().LOGGER.error("Redis connection error for BullMQ", { error });
35
+ });
36
+ redisConnection.on("connect", () => {
37
+ (0, config_1.getConfig)().LOGGER.info("Redis connected for BullMQ");
23
38
  });
24
39
  }
40
+ return redisConnection;
25
41
  }
26
42
  /**
27
- * Start a single pub/sub consumer in this process.
28
- * The `handler` is responsible for business logic (e.g. calling processEvent()).
43
+ * Get or create the notification queue instance
29
44
  */
30
- async function startConsumer(handler, opts) {
31
- if (consumerStarted) {
32
- // eslint-disable-next-line no-console
33
- console.warn("NotificationQueue consumer already started; skipping");
34
- return;
45
+ function getQueue() {
46
+ if (!notificationQueue) {
47
+ const connection = getRedisConnection();
48
+ notificationQueue = new bullmq_1.Queue(QUEUE_NAME, {
49
+ connection,
50
+ defaultJobOptions: {
51
+ backoff: {
52
+ type: "exponential",
53
+ delay: 2000,
54
+ },
55
+ removeOnComplete: { age: 5 * 60 }, // Keep completed jobs for 5 minutes
56
+ removeOnFail: { age: 5 * 60 }, // Remove failed jobs after 5 minutes
57
+ attempts: 1, // Only try once
58
+ },
59
+ });
60
+ // Set up queue event listeners for monitoring
61
+ queueEvents = new bullmq_1.QueueEvents(QUEUE_NAME, { connection });
62
+ queueEvents.on("completed", ({ jobId }) => {
63
+ (0, config_1.getConfig)().LOGGER.info(`Notification job ${jobId} completed`);
64
+ });
65
+ queueEvents.on("failed", ({ jobId, failedReason }) => {
66
+ (0, config_1.getConfig)().LOGGER.error(`Notification job ${jobId} failed`, {
67
+ failedReason,
68
+ });
69
+ });
35
70
  }
36
- consumerStarted = true;
37
- const baseClient = (0, redis_1.getRedisClient)();
38
- const subscriber = baseClient.duplicate();
39
- const channel = opts?.channel ?? DEFAULT_CHANNEL;
71
+ return notificationQueue;
72
+ }
73
+ /**
74
+ * Enqueue an event payload to BullMQ queue for guaranteed delivery.
75
+ * Jobs are processed in parallel across different userIds but maintain FIFO order per userId.
76
+ * This replaces the previous Redis pub/sub implementation.
77
+ */
78
+ async function enqueueEvent(event) {
79
+ const queue = getQueue();
40
80
  try {
41
- await subscriber.connect();
81
+ // Use userId as part of jobId to ensure proper ordering per user
82
+ await queue.add("notification-event", event, {
83
+ jobId: `${event.userId}-${Date.now()}-${Math.random()
84
+ .toString(36)
85
+ .substr(2, 9)}`,
86
+ });
87
+ (0, config_1.getConfig)().LOGGER.info("Notification event enqueued successfully", {
88
+ userId: event.userId,
89
+ });
42
90
  }
43
91
  catch (error) {
44
- // eslint-disable-next-line no-console
45
- console.error("Failed to connect Redis subscriber for notifications", {
92
+ (0, config_1.getConfig)().LOGGER.error("Failed to enqueue notification event", {
46
93
  error,
94
+ event,
47
95
  });
96
+ throw error;
97
+ }
98
+ }
99
+ /**
100
+ * Start a BullMQ worker to process notification events.
101
+ * Jobs are processed in parallel across different userIds but maintain FIFO order per userId.
102
+ * The `handler` is responsible for business logic (e.g. calling processEvent()).
103
+ * This replaces the previous Redis pub/sub consumer implementation.
104
+ */
105
+ async function startConsumer(handler) {
106
+ if (notificationWorker) {
48
107
  return;
49
108
  }
50
- subscriber.on("error", (error) => {
51
- // eslint-disable-next-line no-console
52
- console.error("Redis notification subscriber error", { error });
53
- });
109
+ const connection = getRedisConnection();
54
110
  try {
55
- // ioredis `subscribe` expects an (err, count) callback, not a message handler.
56
- // We subscribe first, then attach a separate "message" listener.
57
- await subscriber.subscribe(channel);
58
- subscriber.on("message", async (subscribedChannel, message) => {
59
- // Only process messages for the intended channel (defensive in case of multiple subscriptions)
60
- if (subscribedChannel !== channel) {
61
- return;
62
- }
63
- let payload;
64
- try {
65
- payload = JSON.parse(message);
66
- }
67
- catch (error) {
68
- // eslint-disable-next-line no-console
69
- console.error("Failed to parse pub/sub notification payload", {
70
- error,
71
- message,
72
- channel: subscribedChannel,
73
- });
74
- return;
75
- }
76
- try {
77
- await handler(payload);
78
- }
79
- catch (error) {
80
- // eslint-disable-next-line no-console
81
- console.error("Failed to process pub/sub notification event", {
82
- error,
83
- payload,
84
- channel: subscribedChannel,
85
- });
111
+ notificationWorker = new bullmq_1.Worker(QUEUE_NAME, async (job) => {
112
+ const payload = job.data;
113
+ const userId = payload.userId;
114
+ // Wait for any existing job for this userId to complete (FIFO per user)
115
+ const previousJob = activeJobsPerUser.get(userId);
116
+ if (previousJob) {
117
+ try {
118
+ await previousJob;
119
+ }
120
+ catch (error) {
121
+ // Previous job failed, but we still want to process this one
122
+ (0, config_1.getConfig)().LOGGER.warn(`Previous job for userId ${userId} failed, continuing with next job`, { error });
123
+ }
86
124
  }
125
+ // Create the promise for this job
126
+ const jobPromise = (async () => {
127
+ try {
128
+ await handler(payload);
129
+ }
130
+ catch (error) {
131
+ // Re-throw to let BullMQ handle retries
132
+ throw error;
133
+ }
134
+ finally {
135
+ // Remove from active jobs when done
136
+ // Since only one job per userId runs at a time, we can safely delete by userId
137
+ activeJobsPerUser.delete(userId);
138
+ }
139
+ })();
140
+ // Store the promise for this userId
141
+ activeJobsPerUser.set(userId, jobPromise);
142
+ // Wait for this job to complete
143
+ await jobPromise;
144
+ }, {
145
+ connection,
146
+ removeOnComplete: { age: 5 * 60 }, // Remove after 5 minutes
147
+ removeOnFail: { age: 5 * 60 }, // Remove failed jobs after 5 minutes
148
+ });
149
+ notificationWorker.on("completed", (job) => {
150
+ (0, config_1.getConfig)().LOGGER.info(`Notification job ${job.id} completed`);
151
+ });
152
+ notificationWorker.on("failed", (job, err) => {
153
+ (0, config_1.getConfig)().LOGGER.error(`Notification job ${job?.id} failed`, {
154
+ error: err,
155
+ attemptsMade: job?.attemptsMade,
156
+ });
157
+ });
158
+ notificationWorker.on("error", (error) => {
159
+ (0, config_1.getConfig)().LOGGER.error("Notification worker error", { error });
87
160
  });
88
161
  }
89
162
  catch (error) {
90
- // eslint-disable-next-line no-console
91
- console.error("Error while subscribing to notification channel", {
163
+ (0, config_1.getConfig)().LOGGER.error("Failed to start notification worker", {
92
164
  error,
93
- channel,
94
165
  });
166
+ throw error;
167
+ }
168
+ }
169
+ /**
170
+ * Gracefully close the worker and queue connections
171
+ */
172
+ async function closeNotificationQueue() {
173
+ const promises = [];
174
+ if (notificationWorker) {
175
+ promises.push(notificationWorker.close());
176
+ notificationWorker = null;
177
+ }
178
+ if (notificationQueue) {
179
+ promises.push(notificationQueue.close());
180
+ notificationQueue = null;
181
+ }
182
+ if (queueEvents) {
183
+ promises.push(queueEvents.close());
184
+ queueEvents = null;
185
+ }
186
+ if (redisConnection) {
187
+ promises.push(redisConnection.quit().then(() => undefined));
188
+ redisConnection = null;
95
189
  }
190
+ await Promise.all(promises);
191
+ (0, config_1.getConfig)().LOGGER.info("Notification queue connections closed");
96
192
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dt-common-device",
3
- "version": "13.0.16",
3
+ "version": "13.0.17",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "files": [