@queuedash/sdk 0.1.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,49 +42,290 @@ 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
- * Queue a job for syncing
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)
284
+ */
285
+ async deleteJob(jobId, queueName) {
286
+ try {
287
+ const controller = new AbortController();
288
+ const timeoutId = setTimeout(() => controller.abort(), this.requestTimeout);
289
+ const response = await fetch(`${this.apiUrl}/api/v1/jobs/${encodeURIComponent(jobId)}?queueName=${encodeURIComponent(queueName)}`, {
290
+ method: "DELETE",
291
+ headers: {
292
+ Authorization: `Bearer ${this.apiKey}`,
293
+ },
294
+ signal: controller.signal,
295
+ });
296
+ clearTimeout(timeoutId);
297
+ if (!response.ok) {
298
+ const errorText = await response.text();
299
+ throw new Error(`HTTP ${response.status}: ${errorText}`);
300
+ }
301
+ }
302
+ catch (error) {
303
+ const err = error instanceof Error ? error : new Error(String(error));
304
+ if (this.onError) {
305
+ this.onError(new Error(`Failed to delete job ${jobId}: ${err.message}`));
306
+ }
307
+ else {
308
+ console.error(`Queuedash: Failed to delete job ${jobId}`, err);
309
+ }
310
+ }
311
+ }
312
+ /**
313
+ * Queue a job for syncing (internal use)
35
314
  */
36
315
  syncJob(job) {
37
- // Check circuit breaker
38
316
  if (this.circuitBreakerOpen) {
39
- this.logger("Circuit breaker open, dropping job");
40
317
  return;
41
318
  }
42
- // Prevent duplicate syncs of same job
43
319
  const jobKey = `${job.queueName}:${job.jobId}`;
44
320
  if (this.syncingJobIds.has(jobKey)) {
45
321
  return;
46
322
  }
47
323
  this.syncingJobIds.add(jobKey);
48
- // Prevent memory leak - drop oldest jobs if queue too large
49
324
  if (this.jobQueue.length >= this.maxQueueSize) {
50
325
  const dropped = this.jobQueue.shift();
51
- 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}`);
52
327
  }
53
328
  this.jobQueue.push(job);
54
- // Flush if batch size reached
55
329
  if (this.jobQueue.length >= this.batchSize) {
56
330
  this.flush();
57
331
  }
@@ -69,7 +343,6 @@ class QueueDashClient {
69
343
  const jobsToSync = [...this.jobQueue];
70
344
  this.jobQueue = [];
71
345
  try {
72
- // Add timeout to fetch
73
346
  const controller = new AbortController();
74
347
  const timeoutId = setTimeout(() => controller.abort(), this.requestTimeout);
75
348
  const response = await fetch(`${this.apiUrl}/api/v1/jobs/sync`, {
@@ -78,9 +351,7 @@ class QueueDashClient {
78
351
  "Content-Type": "application/json",
79
352
  Authorization: `Bearer ${this.apiKey}`,
80
353
  },
81
- body: JSON.stringify({
82
- jobs: jobsToSync,
83
- }),
354
+ body: JSON.stringify({ jobs: jobsToSync }),
84
355
  signal: controller.signal,
85
356
  });
86
357
  clearTimeout(timeoutId);
@@ -90,9 +361,8 @@ class QueueDashClient {
90
361
  }
91
362
  const result = (await response.json());
92
363
  if (!result.success && result.errors) {
93
- console.warn("QueueDash: Some jobs failed to sync:", result.errors);
364
+ console.warn("Queuedash: Some jobs failed to sync:", result.errors);
94
365
  }
95
- // Success - reset counters and clear syncing jobs
96
366
  this.retryCount = 0;
97
367
  this.circuitBreakerFailures = 0;
98
368
  jobsToSync.forEach((job) => {
@@ -103,17 +373,13 @@ class QueueDashClient {
103
373
  this.retryCount++;
104
374
  this.circuitBreakerFailures++;
105
375
  const err = error instanceof Error ? error : new Error(String(error));
106
- // Circuit breaker - open after 10 consecutive failures
107
376
  if (this.circuitBreakerFailures >= 10) {
108
377
  this.openCircuitBreaker();
109
378
  }
110
- // Check if we should retry
111
379
  if (this.retryCount <= this.maxRetries) {
112
- // Put jobs back in queue for retry with exponential backoff
113
380
  this.jobQueue.unshift(...jobsToSync);
114
- const backoffMs = Math.min(1000 * Math.pow(2, this.retryCount - 1), 30000); // Max 30s
115
- this.logger(`Retry ${this.retryCount}/${this.maxRetries} after ${backoffMs}ms`, err);
116
- // 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);
117
383
  setTimeout(() => {
118
384
  if (this.jobQueue.length > 0) {
119
385
  this.flush();
@@ -121,7 +387,6 @@ class QueueDashClient {
121
387
  }, backoffMs);
122
388
  }
123
389
  else {
124
- // Max retries reached - clear syncing jobs and alert
125
390
  this.retryCount = 0;
126
391
  jobsToSync.forEach((job) => {
127
392
  this.syncingJobIds.delete(`${job.queueName}:${job.jobId}`);
@@ -131,7 +396,7 @@ class QueueDashClient {
131
396
  this.onError(new Error(`${errorMsg}: ${err.message}`));
132
397
  }
133
398
  else {
134
- console.error(`QueueDash: ${errorMsg}`, err);
399
+ console.error(`Queuedash: ${errorMsg}`, err);
135
400
  }
136
401
  }
137
402
  }
@@ -143,28 +408,26 @@ class QueueDashClient {
143
408
  if (this.circuitBreakerOpen)
144
409
  return;
145
410
  this.circuitBreakerOpen = true;
146
- console.error("QueueDash: Circuit breaker opened after 10 consecutive failures. Will retry in 60s.");
147
- // Auto-reset after 60 seconds
411
+ console.error("Queuedash: Circuit breaker opened after 10 consecutive failures. Will retry in 60s.");
148
412
  this.circuitBreakerResetTimer = setTimeout(() => {
149
413
  this.circuitBreakerOpen = false;
150
414
  this.circuitBreakerFailures = 0;
151
- console.log("QueueDash: Circuit breaker reset, resuming sync attempts");
415
+ console.log("Queuedash: Circuit breaker reset, resuming sync attempts");
152
416
  }, 60000);
153
417
  if (this.circuitBreakerResetTimer.unref) {
154
418
  this.circuitBreakerResetTimer.unref();
155
419
  }
156
420
  }
157
- logger(message, error) {
421
+ log(message, error) {
158
422
  if (this.onError && error) {
159
- // If onError is provided, use it for warnings too
160
- console.warn(`QueueDash: ${message}`, error);
423
+ console.warn(`Queuedash: ${message}`, error);
161
424
  }
162
425
  else {
163
- console.warn(`QueueDash: ${message}`);
426
+ console.warn(`Queuedash: ${message}`);
164
427
  }
165
428
  }
166
429
  /**
167
- * Stop the client and flush remaining jobs
430
+ * Stop the client, close managed resources, and flush remaining jobs
168
431
  */
169
432
  async stop() {
170
433
  if (this.flushTimer) {
@@ -173,6 +436,15 @@ class QueueDashClient {
173
436
  if (this.circuitBreakerResetTimer) {
174
437
  clearTimeout(this.circuitBreakerResetTimer);
175
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
+ }
176
448
  await this.flush();
177
449
  }
178
450
  startFlushTimer() {
@@ -181,25 +453,23 @@ class QueueDashClient {
181
453
  this.flush();
182
454
  }
183
455
  }, this.flushInterval);
184
- // Don't keep process alive
185
456
  if (this.flushTimer.unref) {
186
457
  this.flushTimer.unref();
187
458
  }
188
459
  }
189
460
  setupShutdownHandlers() {
190
461
  const shutdown = async () => {
191
- console.log("QueueDash: Graceful shutdown - flushing remaining jobs...");
462
+ console.log("Queuedash: Graceful shutdown - flushing remaining jobs...");
192
463
  await this.stop();
193
464
  process.exit(0);
194
465
  };
195
- // Handle graceful shutdown
196
466
  process.once("SIGTERM", shutdown);
197
467
  process.once("SIGINT", shutdown);
198
468
  process.once("beforeExit", () => {
199
469
  if (this.jobQueue.length > 0) {
200
- 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`);
201
471
  }
202
472
  });
203
473
  }
204
474
  }
205
- 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":""}