@quantabit/job-sdk 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/index.cjs ADDED
@@ -0,0 +1,784 @@
1
+ 'use strict';
2
+
3
+ var sdkConfig = require('@quantabit/sdk-config');
4
+ var React = require('react');
5
+
6
+ /**
7
+ * QuantaBit Job SDK - jobApi Service
8
+ *
9
+ * 用于向后端分布式任务中心发送管理指令(如获取队列健康度、触发即时任务、重试失败任务)
10
+ */
11
+
12
+ class JobApiClient extends sdkConfig.BaseApiClient {
13
+ constructor(config = {}) {
14
+ super('/jobs', config);
15
+ }
16
+
17
+ // 获取所有队列的状态与统计信息
18
+ async getQueueStats() {
19
+ return this.get('/stats');
20
+ }
21
+
22
+ // 触发一个后台即时任务
23
+ async triggerJob(jobName, payload = {}) {
24
+ return this.post('/trigger', {
25
+ jobName,
26
+ payload
27
+ });
28
+ }
29
+
30
+ // 停止指定任务
31
+ async cancelJob(jobId) {
32
+ return this.post(`/cancel/${jobId}`);
33
+ }
34
+
35
+ // 重试失败的任务
36
+ async retryJob(jobId) {
37
+ return this.post(`/retry/${jobId}`);
38
+ }
39
+ }
40
+ const jobApi = new JobApiClient();
41
+
42
+ /**
43
+ * QuantaBit Job SDK - useJob Hook
44
+ *
45
+ * 用于获取与管理分布式任务队列状态,支持手动触发、重试任务
46
+ */
47
+
48
+ function useJob() {
49
+ const [queues, setQueues] = React.useState([]);
50
+ const [loading, setLoading] = React.useState(false);
51
+ const [error, setError] = React.useState(null);
52
+ const fetchStats = React.useCallback(async () => {
53
+ setLoading(true);
54
+ setError(null);
55
+ try {
56
+ const data = await jobApi.getQueueStats();
57
+ setQueues(data?.queues || []);
58
+ } catch (err) {
59
+ setError(err);
60
+ // 开发阶段兜底模拟数据
61
+ setQueues([{
62
+ name: 'default-queue',
63
+ active: 3,
64
+ completed: 420,
65
+ failed: 2,
66
+ delayed: 0
67
+ }, {
68
+ name: 'email-sender',
69
+ active: 0,
70
+ completed: 1850,
71
+ failed: 14,
72
+ delayed: 2
73
+ }, {
74
+ name: 'chain-syncer',
75
+ active: 1,
76
+ completed: 95402,
77
+ failed: 1,
78
+ delayed: 0
79
+ }]);
80
+ } finally {
81
+ setLoading(false);
82
+ }
83
+ }, []);
84
+ const trigger = React.useCallback(async (jobName, payload) => {
85
+ setLoading(true);
86
+ try {
87
+ await jobApi.triggerJob(jobName, payload);
88
+ await fetchStats();
89
+ return true;
90
+ } catch (err) {
91
+ setError(err);
92
+ return false;
93
+ } finally {
94
+ setLoading(false);
95
+ }
96
+ }, [fetchStats]);
97
+ const retry = React.useCallback(async jobId => {
98
+ setLoading(true);
99
+ try {
100
+ await jobApi.retryJob(jobId);
101
+ await fetchStats();
102
+ return true;
103
+ } catch (err) {
104
+ setError(err);
105
+ return false;
106
+ } finally {
107
+ setLoading(false);
108
+ }
109
+ }, [fetchStats]);
110
+ React.useEffect(() => {
111
+ fetchStats();
112
+ }, [fetchStats]);
113
+ return {
114
+ queues,
115
+ loading,
116
+ error,
117
+ refresh: fetchStats,
118
+ trigger,
119
+ retry
120
+ };
121
+ }
122
+
123
+ /**
124
+ * QuantaBit Job SDK - i18n support
125
+ */
126
+
127
+ const messages = {
128
+ en: {
129
+ jobMonitor: 'Distributed Job Queue Monitor',
130
+ queueName: 'Queue Name',
131
+ activeJobs: 'Active',
132
+ completedJobs: 'Completed',
133
+ failedJobs: 'Failed',
134
+ delayedJobs: 'Delayed',
135
+ actions: 'Actions',
136
+ triggerJob: 'Trigger Task',
137
+ retryAll: 'Retry Failed',
138
+ status: 'Status',
139
+ lastUpdated: 'Last Updated'
140
+ },
141
+ zh: {
142
+ jobMonitor: '分布式任务队列监视器',
143
+ queueName: '队列名称',
144
+ activeJobs: '活动中',
145
+ completedJobs: '已完成',
146
+ failedJobs: '已失败',
147
+ delayedJobs: '已延迟',
148
+ actions: '操作',
149
+ triggerJob: '手动触发任务',
150
+ retryAll: '重试失败任务',
151
+ status: '状态',
152
+ lastUpdated: '最近更新'
153
+ },
154
+ ja: {
155
+ jobMonitor: '分散ジョブキューモニター',
156
+ queueName: 'キュー名',
157
+ activeJobs: '実行中',
158
+ completedJobs: '完了済み',
159
+ failedJobs: '失敗',
160
+ delayedJobs: '遅延',
161
+ actions: 'アクション',
162
+ triggerJob: 'タスクをトリガー',
163
+ retryAll: '失敗タスクを再試行',
164
+ status: 'ステータス',
165
+ lastUpdated: '最終更新'
166
+ },
167
+ ko: {
168
+ jobMonitor: '분산 작업 큐 모니터',
169
+ queueName: '큐 이름',
170
+ activeJobs: '활성 작업',
171
+ completedJobs: '완료됨',
172
+ failedJobs: '실패함',
173
+ delayedJobs: '지연됨',
174
+ actions: '작업',
175
+ triggerJob: '작업 트리거',
176
+ retryAll: '실패 작업 재시도',
177
+ status: '상태',
178
+ lastUpdated: '최근 업데이트'
179
+ }
180
+ };
181
+ const SUPPORTED_LANGUAGES = ['en', 'zh', 'ja', 'ko'];
182
+ let currentLanguage = 'en';
183
+ function setLanguage(lang) {
184
+ if (SUPPORTED_LANGUAGES.includes(lang)) currentLanguage = lang;
185
+ }
186
+ function getLanguage() {
187
+ return currentLanguage;
188
+ }
189
+ function t(key) {
190
+ return (messages[currentLanguage] || messages.en)[key] || key;
191
+ }
192
+
193
+ /**
194
+ * QuantaBit Job SDK - JobMonitor Component
195
+ *
196
+ * 任务队列监控面板组件,实时展现当前系统的分布式任务引擎吞吐状况,并允许运维触发或重试任务。
197
+ */
198
+
199
+ function JobMonitor({
200
+ className = ''
201
+ }) {
202
+ const {
203
+ queues,
204
+ loading,
205
+ refresh,
206
+ trigger,
207
+ retry
208
+ } = useJob();
209
+ const [showTriggerModal, setShowTriggerModal] = React.useState(false);
210
+ const [targetQueue, setTargetQueue] = React.useState('');
211
+ const [jobPayload, setJobPayload] = React.useState('{}');
212
+ const handleTriggerSubmit = async e => {
213
+ e.preventDefault();
214
+ let payloadObj = {};
215
+ try {
216
+ payloadObj = JSON.parse(jobPayload);
217
+ } catch (_) {
218
+ // 保持空对象
219
+ }
220
+ await trigger(targetQueue, payloadObj);
221
+ setShowTriggerModal(false);
222
+ };
223
+ return /*#__PURE__*/React.createElement("div", {
224
+ className: `qbit-job-monitor ${className}`
225
+ }, /*#__PURE__*/React.createElement("div", {
226
+ className: "qbit-monitor-header"
227
+ }, /*#__PURE__*/React.createElement("h4", null, t('jobMonitor')), /*#__PURE__*/React.createElement("div", {
228
+ className: "qbit-monitor-actions"
229
+ }, /*#__PURE__*/React.createElement("button", {
230
+ className: "qbit-btn-refresh",
231
+ onClick: refresh,
232
+ disabled: loading
233
+ }, loading ? '...' : 'Refresh'))), /*#__PURE__*/React.createElement("div", {
234
+ className: "qbit-monitor-body"
235
+ }, /*#__PURE__*/React.createElement("table", {
236
+ className: "qbit-monitor-table"
237
+ }, /*#__PURE__*/React.createElement("thead", null, /*#__PURE__*/React.createElement("tr", null, /*#__PURE__*/React.createElement("th", null, t('queueName')), /*#__PURE__*/React.createElement("th", null, t('activeJobs')), /*#__PURE__*/React.createElement("th", null, t('completedJobs')), /*#__PURE__*/React.createElement("th", null, t('failedJobs')), /*#__PURE__*/React.createElement("th", null, t('delayedJobs')), /*#__PURE__*/React.createElement("th", null, t('actions')))), /*#__PURE__*/React.createElement("tbody", null, queues.map(q => /*#__PURE__*/React.createElement("tr", {
238
+ key: q.name
239
+ }, /*#__PURE__*/React.createElement("td", {
240
+ className: "qbit-queue-name"
241
+ }, q.name), /*#__PURE__*/React.createElement("td", null, /*#__PURE__*/React.createElement("span", {
242
+ className: "qbit-badge active"
243
+ }, q.active)), /*#__PURE__*/React.createElement("td", null, /*#__PURE__*/React.createElement("span", {
244
+ className: "qbit-badge completed"
245
+ }, q.completed)), /*#__PURE__*/React.createElement("td", null, /*#__PURE__*/React.createElement("span", {
246
+ className: "qbit-badge failed"
247
+ }, q.failed)), /*#__PURE__*/React.createElement("td", null, /*#__PURE__*/React.createElement("span", {
248
+ className: "qbit-badge delayed"
249
+ }, q.delayed)), /*#__PURE__*/React.createElement("td", null, /*#__PURE__*/React.createElement("div", {
250
+ className: "qbit-row-actions"
251
+ }, /*#__PURE__*/React.createElement("button", {
252
+ className: "qbit-btn-trigger",
253
+ onClick: () => {
254
+ setTargetQueue(q.name);
255
+ setShowTriggerModal(true);
256
+ }
257
+ }, t('triggerJob')), q.failed > 0 && /*#__PURE__*/React.createElement("button", {
258
+ className: "qbit-btn-retry",
259
+ onClick: () => retry(q.name)
260
+ }, t('retryAll'))))))))), showTriggerModal && /*#__PURE__*/React.createElement("div", {
261
+ className: "qbit-monitor-modal-backdrop"
262
+ }, /*#__PURE__*/React.createElement("div", {
263
+ className: "qbit-monitor-modal"
264
+ }, /*#__PURE__*/React.createElement("div", {
265
+ className: "qbit-modal-header"
266
+ }, /*#__PURE__*/React.createElement("h5", null, "Trigger Job on [", targetQueue, "]"), /*#__PURE__*/React.createElement("button", {
267
+ className: "close",
268
+ onClick: () => setShowTriggerModal(false)
269
+ }, "\xD7")), /*#__PURE__*/React.createElement("form", {
270
+ onSubmit: handleTriggerSubmit
271
+ }, /*#__PURE__*/React.createElement("div", {
272
+ className: "qbit-form-group"
273
+ }, /*#__PURE__*/React.createElement("label", null, "Job Payload (JSON)"), /*#__PURE__*/React.createElement("textarea", {
274
+ value: jobPayload,
275
+ onChange: e => setJobPayload(e.target.value),
276
+ rows: 4,
277
+ placeholder: "{\"userId\": 101, \"action\": \"sync\"}"
278
+ })), /*#__PURE__*/React.createElement("div", {
279
+ className: "qbit-modal-footer"
280
+ }, /*#__PURE__*/React.createElement("button", {
281
+ type: "button",
282
+ className: "btn-cancel",
283
+ onClick: () => setShowTriggerModal(false)
284
+ }, "Cancel"), /*#__PURE__*/React.createElement("button", {
285
+ type: "submit",
286
+ className: "btn-submit"
287
+ }, "Submit Task"))))), /*#__PURE__*/React.createElement("style", null, `
288
+ .qbit-job-monitor {
289
+ background: #0f172a;
290
+ color: #f8fafc;
291
+ border-radius: 12px;
292
+ border: 1px solid #1e293b;
293
+ font-family: ui-sans-serif, system-ui, -apple-system, sans-serif;
294
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
295
+ overflow: hidden;
296
+ width: 100%;
297
+ }
298
+ .qbit-monitor-header {
299
+ background: #1e293b;
300
+ padding: 16px 20px;
301
+ display: flex;
302
+ justify-content: space-between;
303
+ align-items: center;
304
+ border-bottom: 1px solid #334155;
305
+ }
306
+ .qbit-monitor-header h4 {
307
+ margin: 0;
308
+ font-size: 15px;
309
+ font-weight: 600;
310
+ color: #f1f5f9;
311
+ }
312
+ .qbit-btn-refresh {
313
+ background: transparent;
314
+ border: 1px solid #475569;
315
+ color: #cbd5e1;
316
+ padding: 6px 12px;
317
+ border-radius: 6px;
318
+ font-size: 13px;
319
+ cursor: pointer;
320
+ transition: background 0.2s;
321
+ }
322
+ .qbit-btn-refresh:hover {
323
+ background: #334155;
324
+ }
325
+ .qbit-monitor-body {
326
+ padding: 20px;
327
+ overflow-x: auto;
328
+ }
329
+ .qbit-monitor-table {
330
+ width: 100%;
331
+ border-collapse: collapse;
332
+ text-align: left;
333
+ font-size: 13px;
334
+ }
335
+ .qbit-monitor-table th {
336
+ color: #94a3b8;
337
+ font-weight: 500;
338
+ padding: 10px 12px;
339
+ border-bottom: 1px solid #334155;
340
+ text-transform: uppercase;
341
+ font-size: 11px;
342
+ }
343
+ .qbit-monitor-table td {
344
+ padding: 12px;
345
+ border-bottom: 1px solid #33415540;
346
+ color: #cbd5e1;
347
+ }
348
+ .qbit-queue-name {
349
+ font-family: monospace;
350
+ color: #38bdf8;
351
+ font-weight: bold;
352
+ }
353
+ .qbit-badge {
354
+ display: inline-block;
355
+ padding: 2px 6px;
356
+ border-radius: 4px;
357
+ font-weight: bold;
358
+ font-size: 11px;
359
+ }
360
+ .qbit-badge.active {
361
+ background: rgba(59, 130, 246, 0.15);
362
+ color: #60a5fa;
363
+ }
364
+ .qbit-badge.completed {
365
+ background: rgba(16, 185, 129, 0.15);
366
+ color: #34d399;
367
+ }
368
+ .qbit-badge.failed {
369
+ background: rgba(239, 68, 68, 0.15);
370
+ color: #f87171;
371
+ }
372
+ .qbit-badge.delayed {
373
+ background: rgba(245, 158, 11, 0.15);
374
+ color: #fbbf24;
375
+ }
376
+ .qbit-row-actions {
377
+ display: flex;
378
+ gap: 8px;
379
+ }
380
+ .qbit-btn-trigger, .qbit-btn-retry {
381
+ background: #1e293b;
382
+ border: 1px solid #334155;
383
+ color: #94a3b8;
384
+ padding: 4px 8px;
385
+ border-radius: 4px;
386
+ font-size: 11px;
387
+ cursor: pointer;
388
+ transition: all 0.2s;
389
+ }
390
+ .qbit-btn-trigger:hover {
391
+ background: #3b82f620;
392
+ color: #60a5fa;
393
+ border-color: #3b82f640;
394
+ }
395
+ .qbit-btn-retry:hover {
396
+ background: #ef444420;
397
+ color: #f87171;
398
+ border-color: #ef444440;
399
+ }
400
+ .qbit-monitor-modal-backdrop {
401
+ position: fixed;
402
+ top: 0;
403
+ left: 0;
404
+ right: 0;
405
+ bottom: 0;
406
+ background: rgba(15, 23, 42, 0.7);
407
+ backdrop-filter: blur(4px);
408
+ display: flex;
409
+ align-items: center;
410
+ justify-content: center;
411
+ z-index: 1001;
412
+ }
413
+ .qbit-monitor-modal {
414
+ background: #1e293b;
415
+ color: #f8fafc;
416
+ border-radius: 12px;
417
+ border: 1px solid #334155;
418
+ width: 90%;
419
+ max-width: 440px;
420
+ padding: 20px;
421
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
422
+ }
423
+ .qbit-modal-header {
424
+ display: flex;
425
+ justify-content: space-between;
426
+ align-items: center;
427
+ margin-bottom: 16px;
428
+ }
429
+ .qbit-modal-header h5 {
430
+ margin: 0;
431
+ font-size: 14px;
432
+ color: #f1f5f9;
433
+ }
434
+ .qbit-modal-header .close {
435
+ background: transparent;
436
+ border: none;
437
+ color: #94a3b8;
438
+ font-size: 20px;
439
+ cursor: pointer;
440
+ }
441
+ .qbit-form-group {
442
+ display: flex;
443
+ flex-direction: column;
444
+ gap: 6px;
445
+ margin-bottom: 16px;
446
+ }
447
+ .qbit-form-group label {
448
+ font-size: 11px;
449
+ color: #94a3b8;
450
+ text-transform: uppercase;
451
+ }
452
+ .qbit-form-group textarea {
453
+ background: #0f172a;
454
+ border: 1px solid #334155;
455
+ border-radius: 6px;
456
+ padding: 8px;
457
+ color: #f8fafc;
458
+ font-family: monospace;
459
+ outline: none;
460
+ font-size: 12px;
461
+ }
462
+ .qbit-form-group textarea:focus {
463
+ border-color: #3b82f6;
464
+ }
465
+ .qbit-modal-footer {
466
+ display: flex;
467
+ justify-content: flex-end;
468
+ gap: 8px;
469
+ }
470
+ .btn-cancel, .btn-submit {
471
+ padding: 8px 16px;
472
+ border-radius: 6px;
473
+ font-size: 12px;
474
+ cursor: pointer;
475
+ border: none;
476
+ font-weight: 500;
477
+ }
478
+ .btn-cancel {
479
+ background: transparent;
480
+ color: #cbd5e1;
481
+ border: 1px solid #475569;
482
+ }
483
+ .btn-cancel:hover {
484
+ background: #334155;
485
+ }
486
+ .btn-submit {
487
+ background: #3b82f6;
488
+ color: white;
489
+ }
490
+ .btn-submit:hover {
491
+ background: #2563eb;
492
+ }
493
+ `));
494
+ }
495
+
496
+ var uuid = require('uuid');
497
+ var bullmq = require('bullmq');
498
+ var amqp = require('amqplib');
499
+ class BaseAdapter {
500
+ constructor(config = {}) {
501
+ this.config = config;
502
+ }
503
+ async connect() {
504
+ throw new Error('connect() must be implemented');
505
+ }
506
+ async disconnect() {
507
+ throw new Error('disconnect() must be implemented');
508
+ }
509
+ async publish(queueName, payload, options = {}) {
510
+ throw new Error('publish() must be implemented');
511
+ }
512
+ async consume(queueName, callback, options = {}) {
513
+ throw new Error('consume() must be implemented');
514
+ }
515
+ }
516
+ class RedisAdapter extends BaseAdapter {
517
+ constructor(config = {}) {
518
+ super(config);
519
+ // BullMQ / ioredis connection
520
+ if (!this.config.connection) {
521
+ throw new Error('[RedisAdapter] config.connection is required (e.g. { host, port })');
522
+ }
523
+ this.connection = this.config.connection;
524
+ this.queues = new Map();
525
+ this.workers = new Map();
526
+ }
527
+ async connect() {
528
+ // BullMQ automatically establishes connection upon queue creation
529
+ console.log('[RedisAdapter] Initialized with redis config:', this.connection);
530
+ }
531
+ async disconnect() {
532
+ for (const worker of this.workers.values()) {
533
+ await worker.close();
534
+ }
535
+ for (const queue of this.queues.values()) {
536
+ await queue.close();
537
+ }
538
+ this.workers.clear();
539
+ this.queues.clear();
540
+ console.log('[RedisAdapter] Disonnected gracefully');
541
+ }
542
+ _getQueue(queueName) {
543
+ if (!this.queues.has(queueName)) {
544
+ const q = new bullmq.Queue(queueName, {
545
+ connection: this.connection
546
+ });
547
+ this.queues.set(queueName, q);
548
+ }
549
+ return this.queues.get(queueName);
550
+ }
551
+ async publish(queueName, payload, options = {}) {
552
+ const q = this._getQueue(queueName);
553
+ const jobName = options.jobName || 'defaultJob';
554
+
555
+ // Transform standardize options into BullMQ options
556
+ const bullOptions = {
557
+ jobId: options.fixedId,
558
+ // specific ID for deduplication
559
+ delay: options.delay,
560
+ // In ms
561
+ attempts: options.retries || 3,
562
+ backoff: options.backoff || {
563
+ type: 'exponential',
564
+ delay: 1000
565
+ },
566
+ ...options.adapterOptions
567
+ };
568
+ const job = await q.add(jobName, payload, bullOptions);
569
+ return job.id;
570
+ }
571
+ async consume(queueName, callback, options = {}) {
572
+ if (this.workers.has(queueName)) {
573
+ console.warn(`[RedisAdapter] Worker for queue [${queueName}] already exists.`);
574
+ return;
575
+ }
576
+
577
+ // Bullmq worker implementation
578
+ const worker = new bullmq.Worker(queueName, async job => {
579
+ // Execute standardized callback
580
+ await callback(job.data, job);
581
+ }, {
582
+ connection: this.connection,
583
+ concurrency: options.concurrency || 1,
584
+ ...options.adapterOptions
585
+ });
586
+ worker.on('failed', (job, err) => {
587
+ console.error(`[RedisAdapter] Job ${job?.id} failed in queue [${queueName}]:`, err.message);
588
+ });
589
+ this.workers.set(queueName, worker);
590
+ console.log(`[RedisAdapter] Worker for queue [${queueName}] started (Concurrency: ${options.concurrency || 1})`);
591
+ }
592
+ }
593
+ class RabbitMQAdapter extends BaseAdapter {
594
+ constructor(config = {}) {
595
+ super(config);
596
+ if (!this.config.url) {
597
+ throw new Error('[RabbitMQAdapter] config.url is required (e.g. amqp://host)');
598
+ }
599
+ this.url = this.config.url;
600
+ this.connection = null;
601
+ this.channelPool = new Map(); // queueName -> channel
602
+ }
603
+ async connect() {
604
+ if (!this.connection) {
605
+ try {
606
+ this.connection = await amqp.connect(this.url);
607
+ console.log('[RabbitMQAdapter] Connected to amqp broker:', this.url);
608
+
609
+ // Auto retry logic on unexpected close
610
+ this.connection.on('error', err => {
611
+ console.error('[RabbitMQAdapter] Connection error:', err.message);
612
+ });
613
+ this.connection.on('close', () => {
614
+ console.warn('[RabbitMQAdapter] Connection closed!');
615
+ this.connection = null;
616
+ // You could implement auto-reconnect backoffs here
617
+ });
618
+ } catch (err) {
619
+ console.error('[RabbitMQAdapter] Initial amqp connect failed:', err.message);
620
+ throw err;
621
+ }
622
+ }
623
+ }
624
+ async disconnect() {
625
+ for (const [queueName, channel] of this.channelPool.entries()) {
626
+ await channel.close();
627
+ }
628
+ this.channelPool.clear();
629
+ if (this.connection) {
630
+ await this.connection.close();
631
+ this.connection = null;
632
+ }
633
+ console.log('[RabbitMQAdapter] Disonnected gracefully');
634
+ }
635
+ async _getChannel() {
636
+ if (!this.connection) await this.connect();
637
+ return await this.connection.createChannel();
638
+ }
639
+ async publish(queueName, payload, options = {}) {
640
+ const channel = await this._getChannel();
641
+ await channel.assertQueue(queueName, {
642
+ durable: true
643
+ });
644
+
645
+ // Support specific TTL / delay via rabbitmq x-message-ttl
646
+ // Usually requires delayed-message-exchange plugin for robust delay,
647
+ // this keeps it simple for now standard durable push
648
+ const headers = options.headers || {};
649
+ const sent = channel.sendToQueue(queueName, Buffer.from(JSON.stringify(payload)), {
650
+ persistent: true,
651
+ headers,
652
+ ...options.adapterOptions
653
+ });
654
+ await channel.close();
655
+ return sent ? 'sent' : 'failed';
656
+ }
657
+ async consume(queueName, callback, options = {}) {
658
+ if (this.channelPool.has(queueName)) {
659
+ console.warn(`[RabbitMQAdapter] Worker for queue [${queueName}] already attached.`);
660
+ return;
661
+ }
662
+ const channel = await this._getChannel();
663
+ await channel.assertQueue(queueName, {
664
+ durable: true
665
+ });
666
+ if (options.concurrency) {
667
+ await channel.prefetch(options.concurrency);
668
+ }
669
+ channel.consume(queueName, async msg => {
670
+ if (msg !== null) {
671
+ try {
672
+ const payload = JSON.parse(msg.content.toString());
673
+ await callback(payload, msg);
674
+ channel.ack(msg); // ack securely
675
+ } catch (err) {
676
+ console.error(`[RabbitMQAdapter] Error processing standard message:`, err.message);
677
+ // if re-queue flag isn't disabled, attempt multiple retries manually or drop it to dead-letter exchange
678
+ const requeue = options.requeue !== false;
679
+ channel.nack(msg, false, requeue);
680
+ }
681
+ }
682
+ }, options.adapterOptions);
683
+ this.channelPool.set(queueName, channel);
684
+ console.log(`[RabbitMQAdapter] AMQP channel consuming [${queueName}] (Concurrency: ${options.concurrency || 1})`);
685
+ }
686
+ }
687
+
688
+ /**
689
+ * Universal Job Queue SDK designed for scalable infra.
690
+ */
691
+ class JobQueue {
692
+ /**
693
+ * Initialize SDK with desired engine config.
694
+ * @param {Object} options
695
+ * @param {'redis' | 'rabbitmq'} [options.engine='redis'] - Core transport choice.
696
+ * @param {Object} [options.config] - Driver-specific options block i.e., config.url or config.connection.
697
+ */
698
+ constructor(options = {}) {
699
+ this.engine = options.engine || 'redis';
700
+ this.config = options.config || {};
701
+
702
+ // Plug matching adapter to uniform base footprint
703
+ if (this.engine === 'redis') {
704
+ this.adapter = new RedisAdapter(this.config);
705
+ } else if (this.engine === 'rabbitmq') {
706
+ this.adapter = new RabbitMQAdapter(this.config);
707
+ } else {
708
+ throw new Error(`Unsupported engine: ${this.engine}. Use 'redis' or 'rabbitmq'`);
709
+ }
710
+ }
711
+
712
+ /**
713
+ * Bind connections actively to infrastructure backing service.
714
+ */
715
+ async connect() {
716
+ await this.adapter.connect();
717
+ }
718
+
719
+ /**
720
+ * Drain queue streams and kill connections cleanly.
721
+ */
722
+ async disconnect() {
723
+ await this.adapter.disconnect();
724
+ }
725
+
726
+ /**
727
+ * Push unified shape job payload.
728
+ * @param {string} queueName Queue tag.
729
+ * @param {Object} customData Business domain object to be acted on.
730
+ * @param {Object} options Standard SDK options mapping features consistently across engines.
731
+ * @returns {Promise<string>} Trackable job/execution ID.
732
+ */
733
+ async publishTask(queueName, customData, options = {}) {
734
+ const formattedTask = {
735
+ taskId: uuid.v4(),
736
+ timestamp: Date.now(),
737
+ source: options.source || 'qbit-backend-sdk',
738
+ data: customData || {},
739
+ ...options.metadata
740
+ };
741
+ const executionId = await this.adapter.publish(queueName, formattedTask, options);
742
+ console.log(`[JobQueue][${this.engine}] published task [${formattedTask.taskId}] to [${queueName}]`);
743
+ return executionId || formattedTask.taskId;
744
+ }
745
+
746
+ /**
747
+ * Subscribe uniform pipeline logic against generic engine transport loop.
748
+ *
749
+ * @param {string} queueName Event path.
750
+ * @param {Function} handler The processor async (data, context) => {} -> context exposes tracing.
751
+ * @param {Object} options Extra pipeline overrides matching to underlying plugins mapping cleanly.
752
+ */
753
+ async consumeTask(queueName, handler, options = {}) {
754
+ console.log(`[JobQueue][${this.engine}] Init consuming [${queueName}] with driver mapping.`);
755
+ await this.adapter.consume(queueName, async (rawPayload, rawJob) => {
756
+ // Decode meta contexts & execute the business fn
757
+ const bizData = rawPayload.data || rawPayload;
758
+ const pipelineContext = {
759
+ taskId: rawPayload.taskId,
760
+ source: rawPayload.source,
761
+ timestamp: rawPayload.timestamp,
762
+ metadata: rawPayload,
763
+ // Access extra decorated tags
764
+ raw: rawJob // Break glass back to amqplib/bull node for rare deep interactions
765
+ };
766
+ await handler(bizData, pipelineContext);
767
+ }, options);
768
+ }
769
+ }
770
+ exports.BaseAdapter = BaseAdapter;
771
+ exports.JobQueue = JobQueue;
772
+ exports.RabbitMQAdapter = RabbitMQAdapter;
773
+ exports.RedisAdapter = RedisAdapter;
774
+
775
+ exports.JobApiClient = JobApiClient;
776
+ exports.JobMonitor = JobMonitor;
777
+ exports.SUPPORTED_LANGUAGES = SUPPORTED_LANGUAGES;
778
+ exports.getLanguage = getLanguage;
779
+ exports.jobApi = jobApi;
780
+ exports.messages = messages;
781
+ exports.setLanguage = setLanguage;
782
+ exports.t = t;
783
+ exports.useJob = useJob;
784
+ //# sourceMappingURL=index.cjs.map