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