dt-common-device 3.0.0 → 3.0.1

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.
Files changed (43) hide show
  1. package/README.md +47 -15
  2. package/dist/device/cloud/interface.d.ts +101 -0
  3. package/dist/device/cloud/interface.js +3 -0
  4. package/dist/device/cloud/interfaces/IDeviceConnectionService.d.ts +7 -0
  5. package/dist/device/cloud/interfaces/IDeviceConnectionService.js +3 -0
  6. package/dist/device/cloud/interfaces/IDevicesService.d.ts +9 -0
  7. package/dist/device/cloud/interfaces/IDevicesService.js +2 -0
  8. package/dist/device/cloud/services/Device.service.d.ts +39 -0
  9. package/dist/device/cloud/services/Device.service.js +9 -0
  10. package/dist/device/cloud/services/DeviceCloudService.d.ts +42 -0
  11. package/dist/device/cloud/services/DeviceCloudService.js +59 -0
  12. package/dist/device/cloud/services/DeviceHub.service.d.ts +3 -0
  13. package/dist/device/cloud/services/DeviceHub.service.js +6 -0
  14. package/dist/device/cloud/services/Hub.service.d.ts +25 -0
  15. package/dist/device/cloud/services/Hub.service.js +9 -0
  16. package/dist/device/cloud/services/SmartThingsDeviceService.d.ts +38 -0
  17. package/dist/device/cloud/services/SmartThingsDeviceService.js +52 -0
  18. package/dist/device/index.d.ts +4 -0
  19. package/dist/device/index.js +20 -0
  20. package/dist/device/local/events/EventHandler.js +6 -6
  21. package/dist/device/local/events/Events.d.ts +12 -33
  22. package/dist/device/local/events/Events.js +12 -33
  23. package/dist/device/local/interface.d.ts +0 -0
  24. package/dist/device/local/interface.js +1 -0
  25. package/dist/device/local/services/DeviceHub.service.d.ts +11 -0
  26. package/dist/device/local/services/DeviceHub.service.js +40 -0
  27. package/dist/queue/entities/HybridHttpQueue.d.ts +4 -3
  28. package/dist/queue/entities/HybridHttpQueue.js +95 -43
  29. package/dist/queue/interfaces/IHybridHttpQueue.d.ts +3 -2
  30. package/dist/queue/interfaces/IJobResult.d.ts +8 -0
  31. package/dist/queue/services/QueueService.d.ts +3 -3
  32. package/dist/queue/types/queue.types.d.ts +0 -4
  33. package/dist/queue/utils/queueUtils.js +3 -2
  34. package/dist/queue/utils/rateLimit.utils.d.ts +4 -0
  35. package/dist/queue/utils/rateLimit.utils.js +54 -1
  36. package/package.json +1 -1
  37. package/src/queue/entities/HybridHttpQueue.ts +140 -64
  38. package/src/queue/interfaces/IHybridHttpQueue.ts +3 -2
  39. package/src/queue/interfaces/IJobResult.ts +9 -0
  40. package/src/queue/services/QueueService.ts +3 -3
  41. package/src/queue/types/queue.types.ts +0 -1
  42. package/src/queue/utils/queueUtils.ts +3 -2
  43. package/src/queue/utils/rateLimit.utils.ts +74 -1
@@ -3,17 +3,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.DT_EVENT_TYPES = void 0;
4
4
  exports.DT_EVENT_TYPES = {
5
5
  DEVICE: {
6
- CREATE: {
7
- SUCCESS: "device.create.success",
8
- FAILED: "device.create.failed",
9
- },
10
- UPDATE: {
11
- SUCCESS: "device.update.success",
12
- FAILED: "device.update.failed",
13
- },
14
- DELETE: {
15
- SUCCESS: "device.delete.success",
16
- FAILED: "device.delete.failed",
6
+ DEVICE: {
7
+ CREATED: "device.device.created",
8
+ UPDATED: "device.device.updated",
9
+ DELETED: "device.device.deleted",
17
10
  },
18
11
  STATE: {
19
12
  SET: "device.state.set",
@@ -44,31 +37,17 @@ exports.DT_EVENT_TYPES = {
44
37
  },
45
38
  },
46
39
  CONNECTION: {
47
- CREATE: {
48
- SUCCESS: "connection.create.success",
49
- FAILED: "connection.create.failed",
50
- },
51
- UPDATE: {
52
- SUCCESS: "connection.update.success",
53
- FAILED: "connection.update.failed",
54
- },
55
- DELETE: {
56
- SUCCESS: "connection.delete.success",
57
- FAILED: "connection.delete.failed",
40
+ CONNECTION: {
41
+ CREATED: "connection.connection.created",
42
+ UPDATED: "connection.connection.updated",
43
+ DELETED: "connection.connection.deleted",
58
44
  },
59
45
  },
60
46
  PROPERTY: {
61
- CREATE: {
62
- SUCCESS: "property.create.success",
63
- FAILED: "property.create.failed",
64
- },
65
- UPDATE: {
66
- SUCCESS: "property.update.success",
67
- FAILED: "property.update.failed",
68
- },
69
- DELETE: {
70
- SUCCESS: "property.delete.success",
71
- FAILED: "property.delete.failed",
47
+ PROPERTY: {
48
+ CREATED: "property.property.created",
49
+ UPDATED: "property.property.updated",
50
+ DELETED: "property.property.deleted",
72
51
  },
73
52
  PREFERENCES: {
74
53
  UPDATED: "property.preferences.updated",
File without changes
@@ -0,0 +1 @@
1
+ "use strict";
@@ -0,0 +1,11 @@
1
+ import { IHubCreateParams } from "../interfaces";
2
+ export declare class DeviceHubService {
3
+ private readonly baseUrl;
4
+ constructor();
5
+ addHub(body: IHubCreateParams): Promise<any>;
6
+ getHubs(hubIds: string[]): Promise<any>;
7
+ getHub(hubId: string): Promise<any>;
8
+ updateHub(hubId: string, body: any): Promise<any>;
9
+ deleteHub(hubId: string): Promise<any>;
10
+ deleteAllHubs(hubIds: string[]): Promise<any>;
11
+ }
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.DeviceHubService = void 0;
7
+ const axios_1 = __importDefault(require("axios"));
8
+ const config_1 = require("../../../config/config");
9
+ class DeviceHubService {
10
+ constructor() {
11
+ const { DEVICE_SERVICE } = (0, config_1.getConfig)();
12
+ if (!DEVICE_SERVICE) {
13
+ throw new Error("DEVICE_SERVICE is not configured. Call initialize() first with DEVICE_SERVICE.");
14
+ }
15
+ this.baseUrl = DEVICE_SERVICE;
16
+ }
17
+ async addHub(body) {
18
+ return await axios_1.default.post(`${this.baseUrl}/devices/hubs`, body);
19
+ }
20
+ //get hubs takes an array of hub ids as query params
21
+ async getHubs(hubIds) {
22
+ const query = hubIds && hubIds.length ? `?ids=${hubIds.join(",")}` : "";
23
+ return await axios_1.default.get(`${this.baseUrl}/devices/hubs${query}`);
24
+ }
25
+ //get hub takes a hub id in params
26
+ async getHub(hubId) {
27
+ return await axios_1.default.get(`${this.baseUrl}/devices/hubs/${hubId}`);
28
+ }
29
+ async updateHub(hubId, body) {
30
+ return await axios_1.default.put(`${this.baseUrl}/devices/hubs/${hubId}`, body);
31
+ }
32
+ async deleteHub(hubId) {
33
+ return await axios_1.default.delete(`${this.baseUrl}/devices/hubs/${hubId}`);
34
+ }
35
+ async deleteAllHubs(hubIds) {
36
+ const query = hubIds.length ? `?ids=${hubIds.join(",")}` : "";
37
+ return await axios_1.default.delete(`${this.baseUrl}/devices/hubs${query}`);
38
+ }
39
+ }
40
+ exports.DeviceHubService = DeviceHubService;
@@ -1,11 +1,12 @@
1
1
  import { HttpCallOption } from "../types/http.types";
2
+ import { IQueueResponse } from "../interfaces";
2
3
  export declare class HybridHttpQueue {
3
4
  private readonly queues;
4
5
  private readonly workers;
5
6
  private readonly rateLimitConfigs;
6
7
  private readonly jobResults;
7
8
  constructor();
8
- private handleRateLimit;
9
+ private handleRateLimitAndQueue;
9
10
  private processHttpRequest;
10
11
  request(options: {
11
12
  method: string;
@@ -17,7 +18,7 @@ export declare class HybridHttpQueue {
17
18
  connectionProvider: string;
18
19
  microservice: string;
19
20
  };
20
- }): Promise<any>;
21
- handleRequest(url: string, method: string, options: HttpCallOption): Promise<any>;
21
+ }): Promise<IQueueResponse>;
22
+ handleRequest(url: string, method: string, options: HttpCallOption): Promise<IQueueResponse>;
22
23
  shutdown(): Promise<void>;
23
24
  }
@@ -61,44 +61,69 @@ let HybridHttpQueue = (() => {
61
61
  this.jobResults = new Map();
62
62
  this.rateLimitConfigs = rateLimit_utils_1.RateLimitUtils.initializeRateLimitConfigs();
63
63
  }
64
- async handleRateLimit(job) {
65
- const { connectionId, provider, url, method, options } = job.data;
66
- const microservice = options.queueOptions?.microservice || "default";
67
- const isMaxRetries = job.attemptsMade >= 2;
64
+ async handleRateLimitAndQueue(url, method, options) {
65
+ const { connectionId, provider, microservice } = jobUtils_1.JobUtils.extractConnectionDetails(options);
66
+ const key = `rate_limit:${provider}:${connectionId}`;
67
+ const config = this.rateLimitConfigs.get(provider);
68
+ const windowMs = config?.windowMs ?? 60000;
69
+ const timestamps = await rateLimit_utils_1.RateLimitUtils.getRawRequestTimestamps(key);
70
+ const now = Date.now();
71
+ const windowStart = now - windowMs;
72
+ const recentRequests = timestamps.filter((t) => t > windowStart);
73
+ const nextAvailableTime = recentRequests.length > 0 ? recentRequests[0] + windowMs : now + 1000;
74
+ const delay = Math.max(nextAvailableTime - now, 1000); // at least 1s delay
75
+ // Create job data
76
+ const jobData = {
77
+ microservice,
78
+ connectionId,
79
+ provider,
80
+ url,
81
+ method,
82
+ options,
83
+ timestamp: Date.now(),
84
+ };
85
+ // Add job to queue with delay (background processing)
86
+ const queueKey = queueUtils_1.QueueUtils.getQueueKey(microservice, connectionId, provider);
87
+ const queue = queueUtils_1.QueueUtils.getOrCreateQueue(queueKey, this.queues);
88
+ queueUtils_1.QueueUtils.getOrCreateWorker(queueKey, this.workers, this.processHttpRequest.bind(this), this.jobResults);
89
+ const job = await queue.add("http-request", jobData, {
90
+ delay,
91
+ attempts: 1,
92
+ removeOnComplete: { age: 300, count: 1 },
93
+ removeOnFail: { age: 300, count: 1 },
94
+ });
68
95
  await (0, dt_audit_library_1.publishAudit)({
69
- eventType: isMaxRetries
70
- ? "http.request.failed"
71
- : "http.request.rateLimitExceeded",
96
+ eventType: "http.request.rateLimitQueued",
72
97
  properties: {
98
+ resource: microservice,
73
99
  connectionId,
74
100
  provider,
75
101
  endpoint: url,
76
102
  method,
77
103
  timestamp: Date.now(),
78
104
  queueId: job.id,
79
- reason: isMaxRetries ? "max_retries_exceeded" : "rate_limit_exceeded",
80
- ...(!isMaxRetries && {
81
- retryCount: job.attemptsMade + 1,
82
- maxRetries: 3,
83
- }),
105
+ reason: "rate_limit_exceeded_queued",
106
+ delay,
107
+ estimatedProcessingTime: now + delay,
84
108
  },
85
109
  });
86
- if (isMaxRetries) {
87
- await this.queues
88
- .get(queueUtils_1.QueueUtils.getQueueKey(microservice, connectionId, provider))
89
- ?.obliterate({ force: true });
90
- // Don't throw error for max retries - just return
91
- return;
92
- }
93
- throw new Error("Rate limit exceeded");
110
+ // Return immediate response to controller
111
+ return {
112
+ success: true,
113
+ queued: true,
114
+ estimatedProcessingTime: now + delay,
115
+ jobId: job.id,
116
+ };
94
117
  }
95
118
  async processHttpRequest(job) {
96
119
  const { connectionId, provider, url, method, options } = job.data;
97
- // Check rate limit
98
- if (!(await rateLimit_utils_1.RateLimitUtils.checkRateLimit(connectionId, provider, this.rateLimitConfigs))) {
99
- await this.handleRateLimit(job);
120
+ const allowed = await rateLimit_utils_1.RateLimitUtils.isRateLimitAllowed(connectionId, provider, this.rateLimitConfigs);
121
+ if (!allowed) {
122
+ // This shouldn't happen since we check before queuing, but handle it gracefully
123
+ (0, config_1.getConfig)().LOGGER.warn(`Job ${job.id} still rate limited after delay, skipping`);
100
124
  return;
101
125
  }
126
+ await rateLimit_utils_1.RateLimitUtils.recordRequest(connectionId, provider);
102
127
  try {
103
128
  (0, config_1.getConfig)().LOGGER.info(`Executing HTTP request: ${method} ${url} for ${provider}`);
104
129
  const response = await (0, axios_1.default)({
@@ -143,26 +168,53 @@ let HybridHttpQueue = (() => {
143
168
  }
144
169
  async handleRequest(url, method, options) {
145
170
  const { connectionId, provider, microservice } = jobUtils_1.JobUtils.extractConnectionDetails(options);
146
- const queueKey = queueUtils_1.QueueUtils.getQueueKey(microservice, connectionId, provider);
147
- (0, config_1.getConfig)().LOGGER.info(`Queueing: ${method} ${url} -> ${provider} [${connectionId}]`);
148
- queueUtils_1.QueueUtils.getOrCreateWorker(queueKey, this.workers, this.processHttpRequest.bind(this), this.jobResults);
149
- const queue = queueUtils_1.QueueUtils.getOrCreateQueue(queueKey, this.queues);
150
- const job = await queue.add("http-request", {
151
- microservice,
152
- connectionId,
153
- provider,
154
- url,
155
- method,
156
- options,
157
- timestamp: Date.now(),
158
- }, {
159
- attempts: 3,
160
- backoff: { type: "exponential", delay: 5000 },
161
- removeOnComplete: { age: 300, count: 1 },
162
- removeOnFail: { age: 300, count: 1 },
163
- });
164
- (0, config_1.getConfig)().LOGGER.info(`Job ${job.id} queued, waiting for completion...`);
165
- return jobUtils_1.JobUtils.waitForJobCompletion(job, queueKey, this.jobResults);
171
+ // Check rate limit first
172
+ const allowed = await rateLimit_utils_1.RateLimitUtils.isRateLimitAllowed(connectionId, provider, this.rateLimitConfigs);
173
+ if (!allowed) {
174
+ // Rate limited - queue the request and return immediate response
175
+ return this.handleRateLimitAndQueue(url, method, options);
176
+ }
177
+ // Not rate limited - process immediately
178
+ (0, config_1.getConfig)().LOGGER.info(`Processing immediately: ${method} ${url} -> ${provider} [${connectionId}]`);
179
+ try {
180
+ // Record the request first
181
+ await rateLimit_utils_1.RateLimitUtils.recordRequest(connectionId, provider);
182
+ // Execute the HTTP request
183
+ const response = await (0, axios_1.default)({
184
+ method: method.toLowerCase(),
185
+ url: url,
186
+ headers: options.headers || {},
187
+ timeout: 30000,
188
+ ...(options.body && { data: options.body }),
189
+ ...(options.params && { params: options.params }),
190
+ });
191
+ (0, config_1.getConfig)().LOGGER.info(`HTTP request successful: ${method} ${url} for ${provider}`);
192
+ return {
193
+ success: true,
194
+ data: response.data,
195
+ queued: false,
196
+ };
197
+ }
198
+ catch (error) {
199
+ (0, config_1.getConfig)().LOGGER.error(`HTTP request failed: ${error.message}`);
200
+ await (0, dt_audit_library_1.publishAudit)({
201
+ eventType: "http.request.error",
202
+ properties: {
203
+ connectionId,
204
+ provider,
205
+ endpoint: url,
206
+ method,
207
+ timestamp: Date.now(),
208
+ reason: "execution_error",
209
+ errorMessage: error.message,
210
+ },
211
+ });
212
+ return {
213
+ success: false,
214
+ error: `HTTP request failed: ${error.message}`,
215
+ queued: false,
216
+ };
217
+ }
166
218
  }
167
219
  async shutdown() {
168
220
  (0, config_1.getConfig)().LOGGER.info("Shutting down HTTP queues...");
@@ -1,4 +1,5 @@
1
1
  import { HttpCallOption } from "../types/http.types";
2
+ import { IQueueResponse } from "./IJobResult";
2
3
  export interface IHybridHttpQueue {
3
4
  request(options: {
4
5
  method: string;
@@ -10,7 +11,7 @@ export interface IHybridHttpQueue {
10
11
  connectionProvider: string;
11
12
  microservice: string;
12
13
  };
13
- }): Promise<any>;
14
- handleRequest(url: string, method: string, options: HttpCallOption): Promise<any>;
14
+ }): Promise<IQueueResponse>;
15
+ handleRequest(url: string, method: string, options: HttpCallOption): Promise<IQueueResponse>;
15
16
  shutdown(): Promise<void>;
16
17
  }
@@ -4,3 +4,11 @@ export interface IJobResult {
4
4
  resolved: boolean;
5
5
  timestamp: number;
6
6
  }
7
+ export interface IQueueResponse {
8
+ success: boolean;
9
+ data?: any;
10
+ error?: string;
11
+ queued?: boolean;
12
+ estimatedProcessingTime?: number;
13
+ jobId?: string;
14
+ }
@@ -1,4 +1,4 @@
1
- import { IHybridHttpQueue } from "../interfaces";
1
+ import { IHybridHttpQueue, IQueueResponse } from "../interfaces";
2
2
  import { HttpCallOption } from "../types/http.types";
3
3
  export declare class QueueService implements IHybridHttpQueue {
4
4
  private readonly hybridQueue;
@@ -13,7 +13,7 @@ export declare class QueueService implements IHybridHttpQueue {
13
13
  connectionProvider: string;
14
14
  microservice: string;
15
15
  };
16
- }): Promise<any>;
17
- handleRequest(url: string, method: string, options: HttpCallOption): Promise<any>;
16
+ }): Promise<IQueueResponse>;
17
+ handleRequest(url: string, method: string, options: HttpCallOption): Promise<IQueueResponse>;
18
18
  shutdown(): Promise<void>;
19
19
  }
@@ -13,10 +13,6 @@ export interface QueueConfig {
13
13
  }
14
14
  export interface JobOptions {
15
15
  attempts: number;
16
- backoff: {
17
- type: string;
18
- delay: number;
19
- };
20
16
  removeOnComplete: {
21
17
  age: number;
22
18
  count: number;
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.QueueUtils = void 0;
4
4
  const config_1 = require("../../config/config");
5
+ const redis_1 = require("../../db/redis");
5
6
  class QueueUtils {
6
7
  static getQueueKey(microservice, connectionId, provider) {
7
8
  return `${microservice}_${provider}_${connectionId}`;
@@ -10,7 +11,7 @@ class QueueUtils {
10
11
  return (queues.get(queueKey) ??
11
12
  queues
12
13
  .set(queueKey, new (require("bullmq").Queue)(queueKey, {
13
- connection: require("../../db/redis").getRedisClient(),
14
+ connection: (0, redis_1.getRedisClient)(),
14
15
  }))
15
16
  .get(queueKey));
16
17
  }
@@ -19,7 +20,7 @@ class QueueUtils {
19
20
  return;
20
21
  const { Worker } = require("bullmq");
21
22
  const worker = new Worker(queueKey, processFunction, {
22
- connection: require("../../db/redis").getRedisClient(),
23
+ connection: (0, redis_1.getRedisClient)(),
23
24
  concurrency: 1,
24
25
  removeOnComplete: { age: 300, count: 1 },
25
26
  removeOnFail: { age: 300, count: 1 },
@@ -3,4 +3,8 @@ export declare class RateLimitUtils {
3
3
  private static redisClient;
4
4
  static checkRateLimit(connectionId: string, provider: string, rateLimitConfigs: Map<string, IRateLimitConfig>): Promise<boolean>;
5
5
  static initializeRateLimitConfigs(): Map<string, IRateLimitConfig>;
6
+ static isRateLimitAllowed(connectionId: string, provider: string, rateLimitConfigs: Map<string, IRateLimitConfig>): Promise<boolean>;
7
+ static recordRequest(connectionId: string, provider: string): Promise<void>;
8
+ static getRawRequestTimestamps(key: string): Promise<number[]>;
9
+ static getRateLimitConfig(provider: string): IRateLimitConfig | undefined;
6
10
  }
@@ -33,12 +33,65 @@ class RateLimitUtils {
33
33
  const configs = new Map();
34
34
  // Configure rate limits for different providers
35
35
  configs.set("Sensibo", {
36
- maxRequests: 40,
36
+ maxRequests: 5,
37
37
  windowMs: 60000,
38
38
  provider: "Sensibo",
39
39
  });
40
40
  return configs;
41
41
  }
42
+ static async isRateLimitAllowed(connectionId, provider, rateLimitConfigs) {
43
+ const config = rateLimitConfigs.get(provider);
44
+ if (!config) {
45
+ (0, config_1.getConfig)().LOGGER.warn(`No rate limit config found for provider: ${provider}`);
46
+ return true;
47
+ }
48
+ const key = `rate_limit:${provider}:${connectionId}`;
49
+ const now = Date.now();
50
+ const windowStart = now - config.windowMs;
51
+ try {
52
+ const data = await this.redisClient.get(key);
53
+ const requests = data
54
+ ? JSON.parse(data).filter((t) => t > windowStart)
55
+ : [];
56
+ return requests.length < config.maxRequests;
57
+ }
58
+ catch (error) {
59
+ (0, config_1.getConfig)().LOGGER.error(`Rate limit check error: ${error}`);
60
+ return true;
61
+ }
62
+ }
63
+ static async recordRequest(connectionId, provider) {
64
+ const config = this.getRateLimitConfig(provider);
65
+ if (!config)
66
+ return;
67
+ const key = `rate_limit:${provider}:${connectionId}`;
68
+ const now = Date.now();
69
+ const windowStart = now - config.windowMs;
70
+ try {
71
+ const data = await this.redisClient.get(key);
72
+ const requests = data
73
+ ? JSON.parse(data).filter((t) => t > windowStart)
74
+ : [];
75
+ requests.push(now);
76
+ await this.redisClient.setex(key, Math.ceil(config.windowMs / 1000), JSON.stringify(requests));
77
+ }
78
+ catch (error) {
79
+ (0, config_1.getConfig)().LOGGER.error(`Rate limit record error: ${error}`);
80
+ }
81
+ }
82
+ static async getRawRequestTimestamps(key) {
83
+ try {
84
+ const data = await this.redisClient.get(key);
85
+ return data ? JSON.parse(data) : [];
86
+ }
87
+ catch (error) {
88
+ (0, config_1.getConfig)().LOGGER.error(`Error fetching raw request timestamps: ${error}`);
89
+ return [];
90
+ }
91
+ }
92
+ static getRateLimitConfig(provider) {
93
+ return this.initializeRateLimitConfigs().get(provider);
94
+ }
42
95
  }
43
96
  exports.RateLimitUtils = RateLimitUtils;
44
97
  RateLimitUtils.redisClient = (0, redis_1.getRedisClient)();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dt-common-device",
3
- "version": "3.0.0",
3
+ "version": "3.0.1",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "scripts": {