@queuedash/sdk 0.2.0 → 1.0.0

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.
package/dist/client.js CHANGED
@@ -1,7 +1,40 @@
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
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.QueueDashClient = void 0;
4
- class QueueDashClient {
36
+ exports.Queuedash = void 0;
37
+ class Queuedash {
5
38
  constructor(options) {
6
39
  this.jobQueue = [];
7
40
  this.isFlushing = false;
@@ -9,29 +42,245 @@ class QueueDashClient {
9
42
  this.circuitBreakerFailures = 0;
10
43
  this.circuitBreakerOpen = false;
11
44
  this.syncingJobIds = new Set();
45
+ // Track attached resources for cleanup
46
+ this.managedQueueEvents = [];
47
+ this.attachedQueues = new Set();
12
48
  if (!options.apiKey) {
13
- throw new Error("QueueDash: apiKey is required");
49
+ throw new Error("Queuedash: apiKey is required");
14
50
  }
15
51
  this.apiKey = options.apiKey;
16
- // Hardcoded SaaS API URL - can be overridden for staging only
17
52
  this.apiUrl =
18
- options.__dangerouslyOverrideApiUrl ||
53
+ options.baseUrl ||
19
54
  (process.env.NODE_ENV === "production"
20
55
  ? "https://api.queuedash.com"
21
56
  : "http://localhost:4001");
22
- this.batchSize = options.batchSize || 50;
23
- this.flushInterval = options.flushInterval || 100; // ms
24
- this.maxRetries = options.maxRetries || 5;
25
- this.requestTimeout = options.requestTimeout || 30000; // 30s default
26
- this.maxQueueSize = options.maxQueueSize || 10000; // Prevent memory issues
57
+ this.batchSize = options.batchSize ?? 50;
58
+ this.flushInterval = options.flushInterval ?? 100;
59
+ this.maxRetries = options.maxRetries ?? 5;
60
+ this.requestTimeout = options.requestTimeout ?? 30000;
61
+ this.maxQueueSize = options.maxQueueSize ?? 10000;
27
62
  this.onError = options.onError;
28
- // Start auto-flush timer
29
63
  this.startFlushTimer();
30
- // Graceful shutdown handlers
31
64
  this.setupShutdownHandlers();
32
65
  }
33
66
  /**
34
- * Delete a job (soft delete in QueueDash)
67
+ * Attach a queue or worker for monitoring.
68
+ * Auto-detects the queue library (BullMQ, Bull, Bee-Queue).
69
+ *
70
+ * @example
71
+ * ```ts
72
+ * const qd = new Queuedash({ apiKey: 'qd_...' });
73
+ *
74
+ * // BullMQ - we create QueueEvents internally
75
+ * qd.attach(myBullMQQueue);
76
+ *
77
+ * // Bull
78
+ * qd.attach(myBullQueue);
79
+ *
80
+ * // Bee-Queue
81
+ * qd.attach(myBeeQueue);
82
+ * ```
83
+ */
84
+ attach(resource, options) {
85
+ const type = this.detectType(resource);
86
+ switch (type) {
87
+ case "bullmq-queue":
88
+ this.attachBullMQ(resource, options?.events);
89
+ break;
90
+ case "bullmq-worker":
91
+ this.attachBullMQWorker(resource);
92
+ break;
93
+ case "bull":
94
+ this.attachBull(resource);
95
+ break;
96
+ case "bee":
97
+ this.attachBee(resource);
98
+ break;
99
+ default:
100
+ throw new Error("Queuedash: Unknown queue type. Supported: BullMQ Queue/Worker, Bull, Bee-Queue");
101
+ }
102
+ return this;
103
+ }
104
+ detectType(resource) {
105
+ if (!resource || typeof resource !== "object") {
106
+ return "unknown";
107
+ }
108
+ // BullMQ Queue: has getJob method and opts.connection
109
+ if (typeof resource.getJob === "function" &&
110
+ resource.opts?.connection !== undefined) {
111
+ return "bullmq-queue";
112
+ }
113
+ // BullMQ Worker: has opts.connection but no getJob
114
+ if (resource.opts?.connection !== undefined &&
115
+ typeof resource.getJob !== "function" &&
116
+ resource.name) {
117
+ return "bullmq-worker";
118
+ }
119
+ // Bull: has clients array (redis clients)
120
+ if (Array.isArray(resource.clients)) {
121
+ return "bull";
122
+ }
123
+ // Bee-Queue: has settings object
124
+ if (resource.settings !== undefined && resource.name) {
125
+ return "bee";
126
+ }
127
+ return "unknown";
128
+ }
129
+ async attachBullMQ(queue, existingEvents) {
130
+ const queueName = queue.name;
131
+ if (this.attachedQueues.has(`bullmq:${queueName}`)) {
132
+ console.warn(`Queuedash: Queue "${queueName}" is already attached`);
133
+ return;
134
+ }
135
+ let events;
136
+ // Create QueueEvents if not provided
137
+ if (existingEvents) {
138
+ events = existingEvents;
139
+ }
140
+ else {
141
+ try {
142
+ // Dynamic import to avoid requiring bullmq as a direct dependency
143
+ const { QueueEvents } = await Promise.resolve().then(() => __importStar(require("bullmq")));
144
+ const queueEvents = new QueueEvents(queueName, {
145
+ connection: queue.opts?.connection,
146
+ prefix: queue.redisPrefix,
147
+ });
148
+ this.managedQueueEvents.push(queueEvents);
149
+ events = queueEvents;
150
+ }
151
+ catch (e) {
152
+ throw new Error("Queuedash: Failed to create QueueEvents. Make sure bullmq is installed.");
153
+ }
154
+ }
155
+ this.attachedQueues.add(`bullmq:${queueName}`);
156
+ const syncJobById = (jobId) => {
157
+ queue.getJob(jobId).then((job) => {
158
+ if (job) {
159
+ this.syncJob(this.extractBullMQJobData(job, queueName));
160
+ }
161
+ });
162
+ };
163
+ events.on("waiting", ({ jobId }) => syncJobById(jobId));
164
+ events.on("progress", ({ jobId }) => syncJobById(jobId));
165
+ events.on("completed", ({ jobId }) => syncJobById(jobId));
166
+ events.on("failed", ({ jobId }) => syncJobById(jobId));
167
+ events.on("waiting-children", ({ jobId }) => syncJobById(jobId));
168
+ events.on("added", ({ jobId }) => syncJobById(jobId));
169
+ events.on("active", ({ jobId }) => syncJobById(jobId));
170
+ events.on("delayed", ({ jobId }) => syncJobById(jobId));
171
+ events.on("deduplicated", ({ jobId }) => syncJobById(jobId));
172
+ events.on("removed", ({ jobId }) => this.deleteJob(jobId, queueName));
173
+ events.on("stalled", ({ jobId }) => syncJobById(jobId));
174
+ events.on("retries-exhausted", ({ jobId }) => syncJobById(jobId));
175
+ }
176
+ attachBullMQWorker(_worker) {
177
+ // TODO: Worker monitoring - track active jobs, stalled detection, etc.
178
+ console.warn("Queuedash: Worker monitoring coming soon");
179
+ }
180
+ attachBull(queue) {
181
+ const queueName = queue.name;
182
+ if (this.attachedQueues.has(`bull:${queueName}`)) {
183
+ console.warn(`Queuedash: Queue "${queueName}" is already attached`);
184
+ return;
185
+ }
186
+ this.attachedQueues.add(`bull:${queueName}`);
187
+ queue.on("global:completed", (job) => {
188
+ this.syncJob(this.extractBullJobData(job, queueName));
189
+ });
190
+ queue.on("global:failed", (job) => {
191
+ if (job) {
192
+ this.syncJob(this.extractBullJobData(job, queueName));
193
+ }
194
+ });
195
+ queue.on("active", (job) => {
196
+ this.syncJob(this.extractBullJobData(job, queueName));
197
+ });
198
+ queue.on("progress", (job) => {
199
+ this.syncJob(this.extractBullJobData(job, queueName));
200
+ });
201
+ queue.on("removed", (job) => {
202
+ this.syncJob(this.extractBullJobData(job, queueName));
203
+ });
204
+ }
205
+ attachBee(queue) {
206
+ const queueName = queue.name;
207
+ if (this.attachedQueues.has(`bee:${queueName}`)) {
208
+ console.warn(`Queuedash: Queue "${queueName}" is already attached`);
209
+ return;
210
+ }
211
+ this.attachedQueues.add(`bee:${queueName}`);
212
+ queue.on("succeeded", (job) => {
213
+ const jobData = this.extractBeeJobData(job, queueName);
214
+ jobData.finishedAt = new Date();
215
+ this.syncJob(jobData);
216
+ });
217
+ queue.on("failed", (job, err) => {
218
+ const jobData = this.extractBeeJobData(job, queueName);
219
+ jobData.failedReason = err.message;
220
+ jobData.stacktrace = err.stack ? [err.stack] : undefined;
221
+ jobData.finishedAt = new Date();
222
+ this.syncJob(jobData);
223
+ });
224
+ queue.on("retrying", (job) => {
225
+ const jobData = this.extractBeeJobData(job, queueName);
226
+ jobData.retriedAt = new Date();
227
+ this.syncJob(jobData);
228
+ });
229
+ }
230
+ extractBullMQJobData(job, queueName) {
231
+ return {
232
+ jobId: job.id,
233
+ name: job.name,
234
+ queueName,
235
+ data: job.data,
236
+ opts: job.opts,
237
+ addedAt: new Date(job.timestamp),
238
+ processedAt: job.processedOn ? new Date(job.processedOn) : null,
239
+ finishedAt: job.finishedOn ? new Date(job.finishedOn) : null,
240
+ failedReason: job.failedReason,
241
+ stacktrace: job.stacktrace,
242
+ priority: job.opts?.priority,
243
+ delay: job.opts?.delay,
244
+ timestamp: job.timestamp,
245
+ progress: typeof job.progress === "number" ? job.progress : null,
246
+ };
247
+ }
248
+ extractBullJobData(job, queueName) {
249
+ return {
250
+ jobId: job.id,
251
+ name: job.name || "default",
252
+ queueName,
253
+ data: job.data,
254
+ opts: job.opts,
255
+ addedAt: new Date(job.timestamp),
256
+ processedAt: job.processedOn ? new Date(job.processedOn) : null,
257
+ finishedAt: job.finishedOn ? new Date(job.finishedOn) : null,
258
+ failedReason: job.failedReason,
259
+ stacktrace: job.stacktrace,
260
+ priority: job.opts?.priority,
261
+ delay: job.opts?.delay,
262
+ timestamp: job.timestamp,
263
+ progress: job.progress(),
264
+ };
265
+ }
266
+ extractBeeJobData(job, queueName) {
267
+ return {
268
+ jobId: String(job.id),
269
+ name: "default",
270
+ queueName,
271
+ data: job.data,
272
+ opts: job.options || {},
273
+ addedAt: job.options?.timestamp
274
+ ? new Date(job.options.timestamp)
275
+ : new Date(),
276
+ processedAt: null,
277
+ finishedAt: null,
278
+ priority: null,
279
+ delay: job.options?.delay,
280
+ };
281
+ }
282
+ /**
283
+ * Delete a job (soft delete in Queuedash)
35
284
  */
36
285
  async deleteJob(jobId, queueName) {
37
286
  try {
@@ -56,32 +305,27 @@ class QueueDashClient {
56
305
  this.onError(new Error(`Failed to delete job ${jobId}: ${err.message}`));
57
306
  }
58
307
  else {
59
- console.error(`QueueDash: Failed to delete job ${jobId}`, err);
308
+ console.error(`Queuedash: Failed to delete job ${jobId}`, err);
60
309
  }
61
310
  }
62
311
  }
63
312
  /**
64
- * Queue a job for syncing
313
+ * Queue a job for syncing (internal use)
65
314
  */
66
315
  syncJob(job) {
67
- // Check circuit breaker
68
316
  if (this.circuitBreakerOpen) {
69
- this.logger("Circuit breaker open, dropping job");
70
317
  return;
71
318
  }
72
- // Prevent duplicate syncs of same job
73
319
  const jobKey = `${job.queueName}:${job.jobId}`;
74
320
  if (this.syncingJobIds.has(jobKey)) {
75
321
  return;
76
322
  }
77
323
  this.syncingJobIds.add(jobKey);
78
- // Prevent memory leak - drop oldest jobs if queue too large
79
324
  if (this.jobQueue.length >= this.maxQueueSize) {
80
325
  const dropped = this.jobQueue.shift();
81
- this.logger(`Queue size limit reached (${this.maxQueueSize}), dropping oldest job: ${dropped?.jobId}`);
326
+ this.log(`Queue size limit reached (${this.maxQueueSize}), dropping oldest job: ${dropped?.jobId}`);
82
327
  }
83
328
  this.jobQueue.push(job);
84
- // Flush if batch size reached
85
329
  if (this.jobQueue.length >= this.batchSize) {
86
330
  this.flush();
87
331
  }
@@ -99,7 +343,6 @@ class QueueDashClient {
99
343
  const jobsToSync = [...this.jobQueue];
100
344
  this.jobQueue = [];
101
345
  try {
102
- // Add timeout to fetch
103
346
  const controller = new AbortController();
104
347
  const timeoutId = setTimeout(() => controller.abort(), this.requestTimeout);
105
348
  const response = await fetch(`${this.apiUrl}/api/v1/jobs/sync`, {
@@ -108,9 +351,7 @@ class QueueDashClient {
108
351
  "Content-Type": "application/json",
109
352
  Authorization: `Bearer ${this.apiKey}`,
110
353
  },
111
- body: JSON.stringify({
112
- jobs: jobsToSync,
113
- }),
354
+ body: JSON.stringify({ jobs: jobsToSync }),
114
355
  signal: controller.signal,
115
356
  });
116
357
  clearTimeout(timeoutId);
@@ -120,9 +361,8 @@ class QueueDashClient {
120
361
  }
121
362
  const result = (await response.json());
122
363
  if (!result.success && result.errors) {
123
- console.warn("QueueDash: Some jobs failed to sync:", result.errors);
364
+ console.warn("Queuedash: Some jobs failed to sync:", result.errors);
124
365
  }
125
- // Success - reset counters and clear syncing jobs
126
366
  this.retryCount = 0;
127
367
  this.circuitBreakerFailures = 0;
128
368
  jobsToSync.forEach((job) => {
@@ -133,17 +373,13 @@ class QueueDashClient {
133
373
  this.retryCount++;
134
374
  this.circuitBreakerFailures++;
135
375
  const err = error instanceof Error ? error : new Error(String(error));
136
- // Circuit breaker - open after 10 consecutive failures
137
376
  if (this.circuitBreakerFailures >= 10) {
138
377
  this.openCircuitBreaker();
139
378
  }
140
- // Check if we should retry
141
379
  if (this.retryCount <= this.maxRetries) {
142
- // Put jobs back in queue for retry with exponential backoff
143
380
  this.jobQueue.unshift(...jobsToSync);
144
- const backoffMs = Math.min(1000 * Math.pow(2, this.retryCount - 1), 30000); // Max 30s
145
- this.logger(`Retry ${this.retryCount}/${this.maxRetries} after ${backoffMs}ms`, err);
146
- // Schedule retry
381
+ const backoffMs = Math.min(1000 * Math.pow(2, this.retryCount - 1), 30000);
382
+ this.log(`Retry ${this.retryCount}/${this.maxRetries} after ${backoffMs}ms`, err);
147
383
  setTimeout(() => {
148
384
  if (this.jobQueue.length > 0) {
149
385
  this.flush();
@@ -151,7 +387,6 @@ class QueueDashClient {
151
387
  }, backoffMs);
152
388
  }
153
389
  else {
154
- // Max retries reached - clear syncing jobs and alert
155
390
  this.retryCount = 0;
156
391
  jobsToSync.forEach((job) => {
157
392
  this.syncingJobIds.delete(`${job.queueName}:${job.jobId}`);
@@ -161,7 +396,7 @@ class QueueDashClient {
161
396
  this.onError(new Error(`${errorMsg}: ${err.message}`));
162
397
  }
163
398
  else {
164
- console.error(`QueueDash: ${errorMsg}`, err);
399
+ console.error(`Queuedash: ${errorMsg}`, err);
165
400
  }
166
401
  }
167
402
  }
@@ -173,28 +408,26 @@ class QueueDashClient {
173
408
  if (this.circuitBreakerOpen)
174
409
  return;
175
410
  this.circuitBreakerOpen = true;
176
- console.error("QueueDash: Circuit breaker opened after 10 consecutive failures. Will retry in 60s.");
177
- // Auto-reset after 60 seconds
411
+ console.error("Queuedash: Circuit breaker opened after 10 consecutive failures. Will retry in 60s.");
178
412
  this.circuitBreakerResetTimer = setTimeout(() => {
179
413
  this.circuitBreakerOpen = false;
180
414
  this.circuitBreakerFailures = 0;
181
- console.log("QueueDash: Circuit breaker reset, resuming sync attempts");
415
+ console.log("Queuedash: Circuit breaker reset, resuming sync attempts");
182
416
  }, 60000);
183
417
  if (this.circuitBreakerResetTimer.unref) {
184
418
  this.circuitBreakerResetTimer.unref();
185
419
  }
186
420
  }
187
- logger(message, error) {
421
+ log(message, error) {
188
422
  if (this.onError && error) {
189
- // If onError is provided, use it for warnings too
190
- console.warn(`QueueDash: ${message}`, error);
423
+ console.warn(`Queuedash: ${message}`, error);
191
424
  }
192
425
  else {
193
- console.warn(`QueueDash: ${message}`);
426
+ console.warn(`Queuedash: ${message}`);
194
427
  }
195
428
  }
196
429
  /**
197
- * Stop the client and flush remaining jobs
430
+ * Stop the client, close managed resources, and flush remaining jobs
198
431
  */
199
432
  async stop() {
200
433
  if (this.flushTimer) {
@@ -203,6 +436,15 @@ class QueueDashClient {
203
436
  if (this.circuitBreakerResetTimer) {
204
437
  clearTimeout(this.circuitBreakerResetTimer);
205
438
  }
439
+ // Close any QueueEvents we created
440
+ for (const events of this.managedQueueEvents) {
441
+ try {
442
+ await events.close();
443
+ }
444
+ catch {
445
+ // Ignore close errors
446
+ }
447
+ }
206
448
  await this.flush();
207
449
  }
208
450
  startFlushTimer() {
@@ -211,25 +453,23 @@ class QueueDashClient {
211
453
  this.flush();
212
454
  }
213
455
  }, this.flushInterval);
214
- // Don't keep process alive
215
456
  if (this.flushTimer.unref) {
216
457
  this.flushTimer.unref();
217
458
  }
218
459
  }
219
460
  setupShutdownHandlers() {
220
461
  const shutdown = async () => {
221
- console.log("QueueDash: Graceful shutdown - flushing remaining jobs...");
462
+ console.log("Queuedash: Graceful shutdown - flushing remaining jobs...");
222
463
  await this.stop();
223
464
  process.exit(0);
224
465
  };
225
- // Handle graceful shutdown
226
466
  process.once("SIGTERM", shutdown);
227
467
  process.once("SIGINT", shutdown);
228
468
  process.once("beforeExit", () => {
229
469
  if (this.jobQueue.length > 0) {
230
- console.warn(`QueueDash: Process exiting with ${this.jobQueue.length} unsync'd jobs`);
470
+ console.warn(`Queuedash: Process exiting with ${this.jobQueue.length} unsync'd jobs`);
231
471
  }
232
472
  });
233
473
  }
234
474
  }
235
- exports.QueueDashClient = QueueDashClient;
475
+ exports.Queuedash = Queuedash;
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=client.spec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.spec.d.ts","sourceRoot":"","sources":["../src/client.spec.ts"],"names":[],"mappings":""}