@zintrust/queue-monitor 0.4.60 → 0.4.63

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.
@@ -47,10 +47,6 @@ const pushSnapshot = async (subscription) => {
47
47
  const startPolling = (subscription) => {
48
48
  if (subscription.interval)
49
49
  return;
50
- Logger.debug('Starting QueueMonitoringService polling', {
51
- queue: subscription.config.queue || null,
52
- pattern: subscription.config.pattern,
53
- });
54
50
  void pushSnapshot(subscription);
55
51
  subscription.interval = setInterval(() => {
56
52
  void pushSnapshot(subscription);
@@ -59,10 +55,6 @@ const startPolling = (subscription) => {
59
55
  const stopPolling = (subscription) => {
60
56
  if (!subscription.interval)
61
57
  return;
62
- Logger.debug('Stopping QueueMonitoringService polling', {
63
- queue: subscription.config.queue || null,
64
- pattern: subscription.config.pattern,
65
- });
66
58
  clearInterval(subscription.interval);
67
59
  subscription.interval = null;
68
60
  };
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "@zintrust/queue-monitor",
3
- "version": "0.4.60",
4
- "buildDate": "2026-04-05T07:02:46.309Z",
3
+ "version": "0.4.63",
4
+ "buildDate": "2026-04-05T13:12:54.428Z",
5
5
  "buildEnvironment": {
6
6
  "node": "v22.22.1",
7
7
  "platform": "darwin",
8
8
  "arch": "arm64"
9
9
  },
10
10
  "git": {
11
- "commit": "be058c1d",
11
+ "commit": "533afc24",
12
12
  "branch": "release"
13
13
  },
14
14
  "package": {
@@ -29,8 +29,8 @@
29
29
  "sha256": "26a91f1b41d8a976a9e8fabaedc3fe6dc955c700499f0afc1a16fb91c9848665"
30
30
  },
31
31
  "QueueMonitoringService.js": {
32
- "size": 7011,
33
- "sha256": "23cfc989a6e6926224e3507cb2fad5e6e884070406ca9cf58644536b63a52eb7"
32
+ "size": 6679,
33
+ "sha256": "edd069c1229071078223cd23cbb7bdae9cbb2431ae603ab35f0cfcb7cd1f6ffe"
34
34
  },
35
35
  "api/workerClient.d.ts": {
36
36
  "size": 597,
@@ -41,8 +41,8 @@
41
41
  "sha256": "60f993a42f4a9dd5000b01dae3b0f105a8f4348da8433a735b5cc1929a8f64ce"
42
42
  },
43
43
  "build-manifest.json": {
44
- "size": 3865,
45
- "sha256": "c2769a76a31b33d2de31cb3281ada6e5e41c31c26e4477c05617d02bebaa64af"
44
+ "size": 3866,
45
+ "sha256": "0419f514ce1ea9da7a111680fc03af009afce9a11c61bb909f53e42a09b61690"
46
46
  },
47
47
  "config/queueMonitor.d.ts": {
48
48
  "size": 407,
@@ -73,8 +73,8 @@
73
73
  "sha256": "1b487b6e9cedbcb00cde7e920fb128724cb21aa154160e0bd02b02e2a0bd7334"
74
74
  },
75
75
  "dashboard-ui.js": {
76
- "size": 51371,
77
- "sha256": "955df86422fd6cda793ade860394744cfdb9217ecc7a841ac89dc7027433b834"
76
+ "size": 53771,
77
+ "sha256": "530bdfba8deed3b881ab2b7ea6190b9a521bf4bdcc5a08a386a71b4c031ccb56"
78
78
  },
79
79
  "driver.d.ts": {
80
80
  "size": 707,
@@ -90,7 +90,7 @@
90
90
  },
91
91
  "index.js": {
92
92
  "size": 12811,
93
- "sha256": "a3691ff87403fc468c86efec2d907b45c5c8a642e75a6e6ecf29bdaaf3d220ed"
93
+ "sha256": "eed486a0813866e41d75bac10fdbaba27091d6a07b32561c6176663e9a7dc510"
94
94
  },
95
95
  "metrics.d.ts": {
96
96
  "size": 868,
@@ -420,42 +420,49 @@ const getRenderStatsFunction = () => `
420
420
  grid.removeChild(grid.lastElementChild);
421
421
  }
422
422
  }`;
423
- const getUpdateQueueSelectFunction = () => `
424
- function updateQueueSelect(queues) {
425
- const select = document.getElementById('queue-select');
423
+ const getUpdateQueueSelectHelpersFunction = () => `
424
+ function formatQueueOptionCounts(counts) {
425
+ return counts.waiting + ' waiting, ' + counts.failed + ' failed, ' + counts.completed + ' completed';
426
+ }
427
+
428
+ function getPreferredQueue(select) {
426
429
  const storedQueue = localStorage.getItem(QUEUE_KEY);
427
- const preferredQueue = currentQueue || select.value || storedQueue || '';
430
+ return currentQueue || select.value || storedQueue || '';
431
+ }
428
432
 
429
- if (queues.length === 0) {
430
- select.disabled = true;
431
- if (select.options.length !== 1 || select.options[0].value !== '' || select.options[0].textContent !== 'No queues') {
432
- select.innerHTML = '<option value="">No queues</option>';
433
- }
434
- return '';
433
+ function getEmptyQueueSelection(select) {
434
+ select.disabled = true;
435
+ if (select.options.length !== 1 || select.options[0].value !== '' || select.options[0].textContent !== 'No queues') {
436
+ select.innerHTML = '<option value="">No queues</option>';
435
437
  }
438
+ return '';
439
+ }
436
440
 
437
- const queueNames = queues.map(q => q.name);
441
+ function getDesiredQueueOptions(queues) {
438
442
  const totalWaiting = queues.reduce((acc, queue) => acc + queue.counts.waiting, 0);
439
443
  const totalFailed = queues.reduce((acc, queue) => acc + queue.counts.failed, 0);
444
+ const totalCompleted = queues.reduce((acc, queue) => acc + queue.counts.completed, 0);
440
445
 
441
- const nextQueue = preferredQueue === ALL_QUEUES || queueNames.includes(preferredQueue)
442
- ? preferredQueue
443
- : queueNames[0];
444
- const desiredOptions = [
446
+ return [
445
447
  {
446
448
  value: ALL_QUEUES,
447
- text: 'All queues (' + totalWaiting + ' waiting, ' + totalFailed + ' failed)'
449
+ text: 'All queues (' + totalWaiting + ' waiting, ' + totalFailed + ' failed, ' + totalCompleted + ' completed)'
448
450
  },
449
451
  ...queues.map(q => ({
450
452
  value: q.name,
451
- text: q.name + ' (' + q.counts.waiting + ' waiting, ' + q.counts.failed + ' failed)'
453
+ text: q.name + ' (' + formatQueueOptionCounts(q.counts) + ')'
452
454
  }))
453
455
  ];
456
+ }
454
457
 
458
+ function syncQueueSelectOptions(select, desiredOptions) {
455
459
  const existingOptions = new Map();
456
460
  Array.from(select.options).forEach(option => {
457
461
  existingOptions.set(option.value, option);
458
462
  });
463
+ const existingValues = Array.from(select.options).map(option => option.value);
464
+ const desiredValues = desiredOptions.map(option => option.value);
465
+ const needsStructuralSync = existingValues.length !== desiredValues.length || desiredValues.some((value, index) => existingValues[index] !== value);
459
466
 
460
467
  select.disabled = false;
461
468
 
@@ -466,33 +473,70 @@ const getUpdateQueueSelectFunction = () => `
466
473
  option.value = item.value;
467
474
  }
468
475
 
469
- option.textContent = item.text;
470
- option.selected = item.value === nextQueue;
471
-
472
- const currentAtIndex = select.options[index];
473
- if (currentAtIndex !== option) {
474
- select.appendChild(option);
476
+ if (option.textContent !== item.text) {
477
+ option.textContent = item.text;
475
478
  }
476
- });
477
479
 
478
- Array.from(select.options).forEach(option => {
479
- if (!desiredOptions.some(item => item.value === option.value)) {
480
- option.remove();
480
+ if (needsStructuralSync) {
481
+ const currentAtIndex = select.options[index];
482
+ if (currentAtIndex !== option) {
483
+ select.insertBefore(option, currentAtIndex || null);
484
+ }
481
485
  }
482
486
  });
483
487
 
484
- select.value = nextQueue;
488
+ if (needsStructuralSync) {
489
+ Array.from(select.options).forEach(option => {
490
+ if (!desiredOptions.some(item => item.value === option.value)) {
491
+ option.remove();
492
+ }
493
+ });
494
+ }
495
+ }
496
+ `;
497
+ const getUpdateQueueSelectFunction = () => `
498
+
499
+ function updateQueueSelect(queues) {
500
+ const select = document.getElementById('queue-select');
501
+ const preferredQueue = getPreferredQueue(select);
502
+
503
+ if (queues.length === 0) {
504
+ return getEmptyQueueSelection(select);
505
+ }
506
+
507
+ const queueNames = queues.map(q => q.name);
508
+ const nextQueue = preferredQueue === ALL_QUEUES || queueNames.includes(preferredQueue)
509
+ ? preferredQueue
510
+ : queueNames[0];
511
+ const desiredOptions = getDesiredQueueOptions(queues);
512
+
513
+ if (document.activeElement === select) {
514
+ return nextQueue;
515
+ }
516
+
517
+ syncQueueSelectOptions(select, desiredOptions);
518
+
519
+ if (select.value !== nextQueue) {
520
+ select.value = nextQueue;
521
+ }
485
522
  return nextQueue;
486
523
  }`;
487
524
  const getRenderJobsStateFunction = () => `
488
525
  // Track expanded job IDs to preserve state during SSE updates
489
526
  let expandedJobIds = new Set();
490
527
  `;
491
- const getRenderJobsDetailHelpersFunction = () => `
528
+ const getRenderJobsIdentityHelpersFunction = () => `
492
529
  function getJobId(job) {
493
530
  return String(job.id);
494
531
  }
495
532
 
533
+ function getAdjacentJobDetailRow(row) {
534
+ const existingDetail = row.nextElementSibling;
535
+ return existingDetail && existingDetail.classList.contains('detail-row')
536
+ ? existingDetail
537
+ : null;
538
+ }
539
+
496
540
  function getJobStatusInfo(job) {
497
541
  const status = (job.status || (job.failedReason ? 'failed' : 'completed')).toLowerCase();
498
542
  const statusMap = {
@@ -514,6 +558,8 @@ const getRenderJobsDetailHelpersFunction = () => `
514
558
  }
515
559
  return '<span style="color: var(--muted); font-size: 11px;">—</span>';
516
560
  }
561
+ `;
562
+ const getRenderJobsDetailRowHelpersFunction = () => `
517
563
 
518
564
  function buildJobDetailMarkup(job) {
519
565
  const jobData = {
@@ -539,17 +585,13 @@ const getRenderJobsDetailHelpersFunction = () => `
539
585
  }
540
586
 
541
587
  function removeJobDetailRow(row) {
542
- const existingDetail = row.nextElementSibling;
543
- if (existingDetail && existingDetail.classList.contains('detail-row')) {
588
+ const existingDetail = getAdjacentJobDetailRow(row);
589
+ if (existingDetail) {
544
590
  existingDetail.remove();
545
591
  }
546
592
  }
547
593
 
548
- function upsertJobDetailRow(row, job, parent) {
549
- const existingDetail = row.nextElementSibling && row.nextElementSibling.classList.contains('detail-row')
550
- ? row.nextElementSibling
551
- : null;
552
-
594
+ function buildOrUpdateJobDetailRow(job, existingDetail) {
553
595
  let detailRow = existingDetail;
554
596
  if (!detailRow) {
555
597
  detailRow = document.createElement('tr');
@@ -558,13 +600,22 @@ const getRenderJobsDetailHelpersFunction = () => `
558
600
 
559
601
  detailRow.dataset.jobId = getJobId(job);
560
602
  detailRow.innerHTML = buildJobDetailMarkup(job);
603
+ return detailRow;
604
+ }
605
+
606
+ function upsertJobDetailRow(row, job, parent) {
607
+ const detailRow = buildOrUpdateJobDetailRow(job, getAdjacentJobDetailRow(row));
561
608
 
562
609
  if (parent) {
563
610
  parent.appendChild(detailRow);
564
611
  } else {
565
612
  row.parentNode.insertBefore(detailRow, row.nextSibling);
566
613
  }
614
+
615
+ return detailRow;
567
616
  }
617
+ `;
618
+ const getRenderJobsEmptyStateFunction = () => `
568
619
 
569
620
  function ensureEmptyJobsState(tbody) {
570
621
  if (
@@ -622,6 +673,15 @@ const getRenderJobsRowHelpersFunction = () => `
622
673
  return rows;
623
674
  }
624
675
 
676
+ function getExistingJobDetailRows(tbody) {
677
+ const rows = new Map();
678
+ Array.from(tbody.querySelectorAll('tr.detail-row')).forEach(row => {
679
+ const jobId = row.dataset.jobId;
680
+ if (jobId) rows.set(jobId, row);
681
+ });
682
+ return rows;
683
+ }
684
+
625
685
  function removeObsoleteJobRows(tbody, currentJobIds) {
626
686
  Array.from(tbody.querySelectorAll('tr.expandable-row')).forEach(row => {
627
687
  const jobId = row.dataset.jobId || row.querySelector('code')?.textContent;
@@ -644,9 +704,9 @@ const getRenderJobsFunction = () => `
644
704
  }
645
705
 
646
706
  const currentJobIds = new Set(jobList.map(job => getJobId(job)));
647
- removeObsoleteJobRows(tbody, currentJobIds);
648
-
649
707
  const existingRows = getExistingJobRows(tbody);
708
+ const existingDetailRows = getExistingJobDetailRows(tbody);
709
+ const fragment = document.createDocumentFragment();
650
710
 
651
711
  jobList.forEach((job, idx) => {
652
712
  const jobId = getJobId(job);
@@ -657,15 +717,14 @@ const getRenderJobsFunction = () => `
657
717
  updateExistingJobRow(row, job, idx);
658
718
  }
659
719
 
660
- tbody.appendChild(row);
720
+ fragment.appendChild(row);
661
721
  if (expandedJobIds.has(jobId)) {
662
- upsertJobDetailRow(row, job, tbody);
663
- } else {
664
- removeJobDetailRow(row);
722
+ fragment.appendChild(buildOrUpdateJobDetailRow(job, existingDetailRows.get(jobId)));
665
723
  }
666
724
  });
667
725
 
668
726
  expandedJobIds = new Set([...expandedJobIds].filter(id => currentJobIds.has(id)));
727
+ tbody.replaceChildren(fragment);
669
728
  }`;
670
729
  const getRenderLocksFunction = () => `
671
730
  // Track expanded lock keys to preserve state during SSE updates
@@ -1159,9 +1218,12 @@ const getDashboardScriptFetch = () => `
1159
1218
  `;
1160
1219
  const getDashboardScriptRender = () => [
1161
1220
  getRenderStatsFunction(),
1221
+ getUpdateQueueSelectHelpersFunction(),
1162
1222
  getUpdateQueueSelectFunction(),
1163
1223
  getRenderJobsStateFunction(),
1164
- getRenderJobsDetailHelpersFunction(),
1224
+ getRenderJobsIdentityHelpersFunction(),
1225
+ getRenderJobsDetailRowHelpersFunction(),
1226
+ getRenderJobsEmptyStateFunction(),
1165
1227
  getRenderJobsRowHelpersFunction(),
1166
1228
  getRenderJobsFunction(),
1167
1229
  getRenderLocksFunction(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zintrust/queue-monitor",
3
- "version": "0.4.60",
3
+ "version": "0.4.63",
4
4
  "description": "Queue monitoring package for ZinTrust with BullMQ and Redis.",
5
5
  "private": false,
6
6
  "type": "module",
@@ -20,7 +20,7 @@
20
20
  "node": ">=20.0.0"
21
21
  },
22
22
  "peerDependencies": {
23
- "@zintrust/core": "^0.4.60"
23
+ "@zintrust/core": "^0.4.63"
24
24
  },
25
25
  "publishConfig": {
26
26
  "access": "public"