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