@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/LICENSE +21 -0
- package/README.md +54 -52
- package/dist/client.d.ts +45 -7
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +347 -67
- package/dist/client.spec.d.ts +2 -0
- package/dist/client.spec.d.ts.map +1 -0
- package/dist/client.spec.js +809 -0
- package/dist/index.d.ts +2 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -8
- package/dist/types.d.ts +11 -3
- package/dist/types.d.ts.map +1 -1
- package/package.json +23 -7
- package/dist/integrations/bee.d.ts +0 -8
- package/dist/integrations/bee.d.ts.map +0 -1
- package/dist/integrations/bee.js +0 -43
- package/dist/integrations/bull.d.ts +0 -7
- package/dist/integrations/bull.d.ts.map +0 -1
- package/dist/integrations/bull.js +0 -46
- package/dist/integrations/bullmq.d.ts +0 -7
- package/dist/integrations/bullmq.d.ts.map +0 -1
- package/dist/integrations/bullmq.js +0 -47
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.
|
|
4
|
-
|
|
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("
|
|
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.
|
|
54
|
+
options.baseUrl ||
|
|
19
55
|
(process.env.NODE_ENV === "production"
|
|
20
|
-
? "https://
|
|
21
|
-
: "http://localhost:
|
|
22
|
-
this.batchSize = options.batchSize
|
|
23
|
-
this.flushInterval = options.flushInterval
|
|
24
|
-
this.maxRetries = options.maxRetries
|
|
25
|
-
this.requestTimeout = options.requestTimeout
|
|
26
|
-
this.maxQueueSize = options.maxQueueSize
|
|
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
|
-
*
|
|
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(`
|
|
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.
|
|
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
|
-
//
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
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);
|
|
145
|
-
this.
|
|
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(`
|
|
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("
|
|
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("
|
|
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
|
-
|
|
461
|
+
log(message, error) {
|
|
188
462
|
if (this.onError && error) {
|
|
189
|
-
|
|
190
|
-
console.warn(`QueueDash: ${message}`, error);
|
|
463
|
+
console.warn(`Queuedash: ${message}`, error);
|
|
191
464
|
}
|
|
192
465
|
else {
|
|
193
|
-
console.warn(`
|
|
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("
|
|
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(`
|
|
510
|
+
console.warn(`Queuedash: Process exiting with ${this.jobQueue.length} unsync'd jobs`);
|
|
231
511
|
}
|
|
232
512
|
});
|
|
233
513
|
}
|
|
234
514
|
}
|
|
235
|
-
exports.
|
|
515
|
+
exports.Queuedash = Queuedash;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.spec.d.ts","sourceRoot":"","sources":["../src/client.spec.ts"],"names":[],"mappings":""}
|