@zintrust/queue-monitor 1.8.0 → 2.0.1

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,20 @@
1
+ type WorkerApiResponse<T> = {
2
+ ok: boolean;
3
+ error?: string;
4
+ } & T;
5
+ export declare const WorkerClient: Readonly<{
6
+ listWorkers(): Promise<string[]>;
7
+ getWorker(name: string): Promise<unknown>;
8
+ getStatus(name: string): Promise<unknown>;
9
+ getHealth(name: string): Promise<unknown>;
10
+ startWorker(name: string): Promise<WorkerApiResponse<{
11
+ message?: string;
12
+ }>>;
13
+ stopWorker(name: string): Promise<WorkerApiResponse<{
14
+ message?: string;
15
+ }>>;
16
+ restartWorker(name: string): Promise<WorkerApiResponse<{
17
+ message?: string;
18
+ }>>;
19
+ }>;
20
+ export {};
@@ -0,0 +1,45 @@
1
+ import { ErrorFactory, Logger } from '@zintrust/core';
2
+ import { WorkerConfig } from '../config/workerConfig.js';
3
+ const requestJson = async (path, options = {}) => {
4
+ const baseUrl = WorkerConfig.getWorkerBaseUrl();
5
+ const url = `${baseUrl}${path}`;
6
+ const response = await fetch(url, {
7
+ ...options,
8
+ headers: {
9
+ 'Content-Type': 'application/json',
10
+ ...options.headers,
11
+ },
12
+ });
13
+ if (!response.ok) {
14
+ Logger.error('Worker API request failed', { url, status: response.status });
15
+ throw ErrorFactory.createWorkerError(`Worker API request failed (${response.status})`);
16
+ }
17
+ return (await response.json());
18
+ };
19
+ export const WorkerClient = Object.freeze({
20
+ async listWorkers() {
21
+ const response = await requestJson('/api/workers');
22
+ return response.workers ?? [];
23
+ },
24
+ async getWorker(name) {
25
+ const response = await requestJson(`/api/workers/${name}`);
26
+ return response.worker;
27
+ },
28
+ async getStatus(name) {
29
+ const response = await requestJson(`/api/workers/${name}/status`);
30
+ return response.status;
31
+ },
32
+ async getHealth(name) {
33
+ const response = await requestJson(`/api/workers/${name}/health`);
34
+ return response.health;
35
+ },
36
+ async startWorker(name) {
37
+ return requestJson(`/api/workers/${name}/start`, { method: 'POST' });
38
+ },
39
+ async stopWorker(name) {
40
+ return requestJson(`/api/workers/${name}/stop`, { method: 'POST' });
41
+ },
42
+ async restartWorker(name) {
43
+ return requestJson(`/api/workers/${name}/restart`, { method: 'POST' });
44
+ },
45
+ });
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "@zintrust/queue-monitor",
3
- "version": "1.8.0",
4
- "buildDate": "2026-05-04T17:53:03.522Z",
3
+ "version": "2.0.0",
4
+ "buildDate": "2026-05-22T14:51:47.293Z",
5
5
  "buildEnvironment": {
6
- "node": "v20.20.2",
7
- "platform": "linux",
8
- "arch": "x64"
6
+ "node": "v22.22.1",
7
+ "platform": "darwin",
8
+ "arch": "arm64"
9
9
  },
10
10
  "git": {
11
- "commit": "b1d6ed34",
12
- "branch": "master"
11
+ "commit": "3c553e92",
12
+ "branch": "release"
13
13
  },
14
14
  "package": {
15
15
  "engines": {
@@ -40,6 +40,34 @@
40
40
  "size": 8928,
41
41
  "sha256": "fc408db720d5883821b169eabce66944295068cd6f2338f1884493c299f0a0f1"
42
42
  },
43
+ "api/workerClient.d.ts": {
44
+ "size": 597,
45
+ "sha256": "1d712bfa9127aa2df4f1fbd0efbb6d84069e1888ed73aca5542a41eee865d4bb"
46
+ },
47
+ "api/workerClient.js": {
48
+ "size": 1629,
49
+ "sha256": "60f993a42f4a9dd5000b01dae3b0f105a8f4348da8433a735b5cc1929a8f64ce"
50
+ },
51
+ "build-manifest.json": {
52
+ "size": 4136,
53
+ "sha256": "382a0dce0ff1c6077eece1a71780eb91726048370dc6a61fe9a80c1212917082"
54
+ },
55
+ "config/queueMonitor.d.ts": {
56
+ "size": 407,
57
+ "sha256": "4541f47e64c8ede1bfd8fc0cb7edb76c4e885311b28b1f51c9be7639e5d87eca"
58
+ },
59
+ "config/queueMonitor.js": {
60
+ "size": 689,
61
+ "sha256": "0b95e6b65d4b6ffdd69788cdfd19e0e76400a39ad69dce018d8827a3b298e419"
62
+ },
63
+ "config/workerConfig.d.ts": {
64
+ "size": 86,
65
+ "sha256": "b669205d50c8844455a2d9b34a54f48a71118eb6ac99bad5372683ab666f5a22"
66
+ },
67
+ "config/workerConfig.js": {
68
+ "size": 628,
69
+ "sha256": "ca1c6dbaa751893f0e6b7c8a7fd41a80f7d5e8fc9aaaa4877ca12821bb25f56f"
70
+ },
43
71
  "connection.d.ts": {
44
72
  "size": 285,
45
73
  "sha256": "02b8d856e6b905f10ac01fabe7c44560530878c0ec73924889e1d159018d317a"
@@ -48,6 +76,14 @@
48
76
  "size": 171,
49
77
  "sha256": "a4d2967491dc471a75c163f81b5c7199f9e3b34c7c958b65d5bc6bb11798fc6f"
50
78
  },
79
+ "dashboard-index.d.ts": {
80
+ "size": 180,
81
+ "sha256": "f78b4a0a83566640eca154f92d3f858ec41deb2521a500939ee3f0e58ee5db86"
82
+ },
83
+ "dashboard-index.js": {
84
+ "size": 183,
85
+ "sha256": "9d7c7fc63fe2919de0584d3f3d0ef74166d834af0a68caca2fe6fe64340652b9"
86
+ },
51
87
  "dashboard-ui.d.ts": {
52
88
  "size": 219,
53
89
  "sha256": "5140191207b5620500ac5e1edacefed65956a1ad88739695d545ea7d2b405243"
@@ -56,6 +92,14 @@
56
92
  "size": 53927,
57
93
  "sha256": "71bab126f20aea1165ee3aa3c82e896ebd89b1f1a815ef2716cd884b846ab34f"
58
94
  },
95
+ "driver-index.d.ts": {
96
+ "size": 280,
97
+ "sha256": "71a4dc5d823391fe16e966069aae8e6704d5c2d64aaceb447e138fdde7023690"
98
+ },
99
+ "driver-index.js": {
100
+ "size": 184,
101
+ "sha256": "fb91df973acd13a88cca443e50ecdd34944c66e1a1d48333f63099079a00f3c9"
102
+ },
59
103
  "driver.d.ts": {
60
104
  "size": 1102,
61
105
  "sha256": "70b74e7a0489da3a5545dfd8458ab5b4def1f3733c291a782c033fed0afd0bcf"
@@ -70,7 +114,15 @@
70
114
  },
71
115
  "index.js": {
72
116
  "size": 14399,
73
- "sha256": "ddfa566b4eb551edde05d0ea56432aaa678a5bf61737503a6af1cae7c40db7bb"
117
+ "sha256": "150a5729d3250b7351a1282ce74d923ee6e0f43bee85deed86ad843f664521c4"
118
+ },
119
+ "metrics-index.d.ts": {
120
+ "size": 233,
121
+ "sha256": "993ffe832f2914a81c81364f7b6d5c7247cd16da9bb9feb04caf8d770c14f9ec"
122
+ },
123
+ "metrics-index.js": {
124
+ "size": 171,
125
+ "sha256": "53e858995605858b819f2c48c701e3b8f7d028ec51c1b4814c46284c7fb3989e"
74
126
  },
75
127
  "metrics.d.ts": {
76
128
  "size": 910,
@@ -80,6 +132,22 @@
80
132
  "size": 3472,
81
133
  "sha256": "fab0e6d62ed694dcf57904a68b683280e4063c2fdcf97c60fdf5d933ae2b8f4f"
82
134
  },
135
+ "routes/workers.d.ts": {
136
+ "size": 477,
137
+ "sha256": "cfcc3527c47d1a796a3ced2d4777b339dd767f7feb3ecc66c5ce0a91d8ff1bc8"
138
+ },
139
+ "routes/workers.js": {
140
+ "size": 901,
141
+ "sha256": "1ec48c0becc8c0628b1780fb400e957c5fe50da79f73112436c33ecde7d7d574"
142
+ },
143
+ "runtime-index.d.ts": {
144
+ "size": 2209,
145
+ "sha256": "e5d5c3475a7caec57f9e1a61bffb11038858e2c3f7be03e04ca28b8295e878fc"
146
+ },
147
+ "runtime-index.js": {
148
+ "size": 8759,
149
+ "sha256": "9c08789ef9e4da7cfcf431cd369808f9baa088381e58ada3daa5e91f6e034e5d"
150
+ },
83
151
  "worker.d.ts": {
84
152
  "size": 332,
85
153
  "sha256": "97cddbc991c6e6724950090cd92f06671932ab848c16dd94a030d9fba3fbae26"
@@ -87,6 +155,14 @@
87
155
  "worker.js": {
88
156
  "size": 1233,
89
157
  "sha256": "da26c1b80da7473e1644a4e0f8d814097ce0adff14e36037164f9ae5855e656a"
158
+ },
159
+ "workers-ui.d.ts": {
160
+ "size": 214,
161
+ "sha256": "4f5fd3bbd077d0dee2cc6c73d2c736fc52119b0d3a5f4a1878a4f3d3d3edf299"
162
+ },
163
+ "workers-ui.js": {
164
+ "size": 24928,
165
+ "sha256": "ba2c5cbf890fe875cc5336c76bd29b146089bc8ba3a70de4a2c383f395f9be73"
90
166
  }
91
167
  }
92
168
  }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Queue Monitor Configuration (default override)
3
+ *
4
+ * Keep this file declarative:
5
+ * - Core owns env parsing/default logic.
6
+ * - Projects can override config by editing values below.
7
+ */
8
+ declare const _default: {
9
+ enabled: boolean;
10
+ basePath: string;
11
+ middleware: string[];
12
+ redis: {
13
+ host: string;
14
+ port: number;
15
+ password: string;
16
+ };
17
+ };
18
+ export default _default;
@@ -0,0 +1,21 @@
1
+ import { Env } from '@zintrust/core';
2
+ /**
3
+ * Queue Monitor Configuration (default override)
4
+ *
5
+ * Keep this file declarative:
6
+ * - Core owns env parsing/default logic.
7
+ * - Projects can override config by editing values below.
8
+ */
9
+ export default {
10
+ enabled: Env.getBool('QUEUE_MONITOR_ENABLED', true),
11
+ basePath: Env.get('QUEUE_MONITOR_BASE_PATH', '/queue-monitor'),
12
+ middleware: Env.get('QUEUE_MONITOR_MIDDLEWARE', 'auth')
13
+ .split(',')
14
+ .map((m) => m.trim())
15
+ .filter((m) => m.length > 0),
16
+ redis: {
17
+ host: Env.get('REDIS_HOST', 'localhost'),
18
+ port: Env.getInt('REDIS_PORT', 6379),
19
+ password: Env.get('REDIS_PASSWORD', ''),
20
+ },
21
+ };
@@ -0,0 +1,3 @@
1
+ export declare const WorkerConfig: Readonly<{
2
+ getWorkerBaseUrl: () => string;
3
+ }>;
@@ -0,0 +1,19 @@
1
+ import { Env } from '@zintrust/core';
2
+ const normalizeBaseUrl = (value) => {
3
+ let end = value.length;
4
+ while (end > 0 && value.charAt(end - 1) === '/') {
5
+ end--;
6
+ }
7
+ return value.slice(0, end);
8
+ };
9
+ const withHttpScheme = (value) => value.startsWith('http://') || value.startsWith('https://') ? value : `http://${value}`;
10
+ const resolveWorkerApiUrl = () => {
11
+ const workerApiUrl = Env.get('WORKER_API_URL');
12
+ if (workerApiUrl) {
13
+ return normalizeBaseUrl(withHttpScheme(workerApiUrl));
14
+ }
15
+ return '';
16
+ };
17
+ export const WorkerConfig = Object.freeze({
18
+ getWorkerBaseUrl: resolveWorkerApiUrl,
19
+ });
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Queue Monitor Dashboard UI - Non-runtime entrypoint
3
+ * Contains the dashboard UI components for development/admin use
4
+ */
5
+ export { getDashboardHtml } from './dashboard-ui';
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Queue Monitor Dashboard UI - Non-runtime entrypoint
3
+ * Contains the dashboard UI components for development/admin use
4
+ */
5
+ export { getDashboardHtml } from './dashboard-ui';
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Queue Monitor Driver - Runtime-only entrypoint
3
+ * Contains only the core queue driver functionality for production Workers
4
+ */
5
+ export type { JobPayload, JobCounts, RetrySnapshot, RetryJobResult, QueueDriver } from './driver';
6
+ export { createBullMQDriver } from './driver';
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Queue Monitor Driver - Runtime-only entrypoint
3
+ * Contains only the core queue driver functionality for production Workers
4
+ */
5
+ export { createBullMQDriver } from './driver';
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Queue Monitor Metrics - Runtime-only entrypoint
3
+ * Contains only the metrics functionality for production Workers
4
+ */
5
+ export type { JobStatus, JobSummary, Metrics } from './metrics';
6
+ export { createMetrics } from './metrics';
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Queue Monitor Metrics - Runtime-only entrypoint
3
+ * Contains only the metrics functionality for production Workers
4
+ */
5
+ export { createMetrics } from './metrics';
@@ -0,0 +1,10 @@
1
+ import { type IRouter } from '@zintrust/core';
2
+ import { type WorkerUiOptions } from '../workers-ui';
3
+ type RouteOptions = {
4
+ middleware?: ReadonlyArray<string>;
5
+ } | undefined;
6
+ export declare const registerWorkerUiRoutes: (router: IRouter, options: WorkerUiOptions, routeOptions: RouteOptions) => void;
7
+ declare const _default: Readonly<{
8
+ registerWorkerUiRoutes: (router: IRouter, options: WorkerUiOptions, routeOptions: RouteOptions) => void;
9
+ }>;
10
+ export default _default;
@@ -0,0 +1,20 @@
1
+ import { Router } from '@zintrust/core';
2
+ import { WorkerConfig } from '../config/workerConfig.js';
3
+ import { getWorkersHtml } from '../workers-ui.js';
4
+ const registerWorkerUiPage = (router, options, routeOptions) => {
5
+ const handler = (_req, res) => {
6
+ res.html(getWorkersHtml({
7
+ basePath: options.basePath,
8
+ apiBaseUrl: WorkerConfig.getWorkerBaseUrl(),
9
+ autoRefresh: options.autoRefresh,
10
+ refreshIntervalMs: options.refreshIntervalMs,
11
+ }));
12
+ };
13
+ Router.get(router, `${options.basePath}/workers`, handler, routeOptions);
14
+ Router.get(router, '/workers', handler, routeOptions);
15
+ Router.get(router, '/workers/', handler, routeOptions);
16
+ };
17
+ export const registerWorkerUiRoutes = (router, options, routeOptions) => {
18
+ registerWorkerUiPage(router, options, routeOptions);
19
+ };
20
+ export default Object.freeze({ registerWorkerUiRoutes });
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Queue Monitor - Runtime-only entrypoint for production Workers
3
+ * Excludes dashboard UI components that are not needed for runtime
4
+ */
5
+ import { type RedisConfig } from './connection';
6
+ import { type QueueDriver } from './driver';
7
+ import { type Metrics } from './metrics';
8
+ export type { JobPayload } from './driver';
9
+ export { createMetrics, type JobStatus, type JobSummary, type Metrics } from './metrics';
10
+ export { createWorker as createQueueWorker, type QueueWorker } from './worker';
11
+ export type QueueMonitorConfig = {
12
+ enabled?: boolean;
13
+ basePath?: string;
14
+ middleware?: ReadonlyArray<string>;
15
+ autoRefresh?: boolean;
16
+ refreshIntervalMs?: number;
17
+ redis?: RedisConfig;
18
+ knownQueues?: ReadonlyArray<string> | (() => Promise<ReadonlyArray<string>> | ReadonlyArray<string>);
19
+ };
20
+ export type QueueCounts = {
21
+ waiting: number;
22
+ active: number;
23
+ completed: number;
24
+ failed: number;
25
+ delayed: number;
26
+ paused: number;
27
+ };
28
+ export type QueueMonitorSnapshot = {
29
+ status: 'ok';
30
+ startedAt: string;
31
+ queues: Array<{
32
+ name: string;
33
+ counts: QueueCounts;
34
+ }>;
35
+ };
36
+ export type LockSummary = {
37
+ key: string;
38
+ ttl?: number;
39
+ expires?: string;
40
+ };
41
+ export type LockMetrics = {
42
+ active: number;
43
+ attempts: number;
44
+ acquired: number;
45
+ collisions: number;
46
+ collisionRate: number;
47
+ };
48
+ export type LockHistogramBucket = {
49
+ label: string;
50
+ count: number;
51
+ };
52
+ export type LockAnalytics = {
53
+ locks: LockSummary[];
54
+ metrics: LockMetrics;
55
+ histogram: LockHistogramBucket[];
56
+ };
57
+ export type QueueMonitorApi = {
58
+ getSnapshot: () => Promise<QueueMonitorSnapshot>;
59
+ getLocks: (pattern?: string) => Promise<LockAnalytics>;
60
+ driver: QueueDriver;
61
+ metrics: Metrics;
62
+ close: () => Promise<void>;
63
+ };
64
+ export declare const QueueMonitor: Readonly<{
65
+ create(config: QueueMonitorConfig): QueueMonitorApi;
66
+ }>;
67
+ export default QueueMonitor;
68
+ export { createBullMQDriver } from './driver';
69
+ /**
70
+ * Package version and build metadata
71
+ * Available at runtime for debugging and health checks
72
+ */
73
+ export declare const _ZINTRUST_QUEUE_MONITOR_VERSION = "0.1.0";
74
+ export declare const _ZINTRUST_QUEUE_MONITOR_BUILD_DATE = "__BUILD_DATE__";
@@ -0,0 +1,234 @@
1
+ /**
2
+ * Queue Monitor - Runtime-only entrypoint for production Workers
3
+ * Excludes dashboard UI components that are not needed for runtime
4
+ */
5
+ import { isNonEmptyString, Logger, queueConfig, resolveLockPrefix, ShutdownTrace, } from '@zintrust/core';
6
+ import { createRedisConnection } from './connection';
7
+ import { createBullMQDriver } from './driver';
8
+ import { createMetrics } from './metrics';
9
+ export { createMetrics } from './metrics';
10
+ export { createWorker as createQueueWorker } from './worker';
11
+ const DEFAULTS = {
12
+ enabled: true,
13
+ basePath: '/queue-monitor',
14
+ middleware: [],
15
+ autoRefresh: true,
16
+ refreshIntervalMs: 5000,
17
+ };
18
+ const METRICS_KEYS = {
19
+ attempts: 'metrics:attempts',
20
+ acquired: 'metrics:acquired',
21
+ collisions: 'metrics:collisions',
22
+ };
23
+ const HISTOGRAM_BUCKETS = [
24
+ { label: '<30s', max: 30_000 },
25
+ { label: '30s-2m', max: 120_000 },
26
+ { label: '2-10m', max: 600_000 },
27
+ { label: '10-60m', max: 3_600_000 },
28
+ { label: '>60m', min: 3_600_000 },
29
+ ];
30
+ const MAX_LOCK_KEYS = 10_000;
31
+ function normalizeQueueNames(queueNames) {
32
+ return Array.from(new Set(queueNames
33
+ .filter((queueName) => typeof queueName === 'string' && isNonEmptyString(queueName))
34
+ .map((name) => name.trim())))
35
+ .filter((name) => name.length > 0)
36
+ .sort((left, right) => left.localeCompare(right));
37
+ }
38
+ async function resolveKnownQueues(knownQueues) {
39
+ if (typeof knownQueues === 'function') {
40
+ return normalizeQueueNames(await knownQueues());
41
+ }
42
+ if (Array.isArray(knownQueues)) {
43
+ return normalizeQueueNames(knownQueues);
44
+ }
45
+ return [];
46
+ }
47
+ // Helper function to scan lock keys with pagination
48
+ const scanLockKeys = async (client, searchPattern, maxKeys) => {
49
+ const keys = [];
50
+ let cursor = '0';
51
+ do {
52
+ // Redis scan must be sequential
53
+ // eslint-disable-next-line no-await-in-loop
54
+ const [nextCursor, batch] = await client.scan(cursor, 'MATCH', searchPattern, 'COUNT', '200');
55
+ cursor = nextCursor;
56
+ keys.push(...batch);
57
+ if (keys.length >= maxKeys) {
58
+ Logger.warn('Lock scan limit reached', {
59
+ pattern: searchPattern,
60
+ keysFound: keys.length,
61
+ });
62
+ break;
63
+ }
64
+ } while (cursor !== '0');
65
+ return keys;
66
+ };
67
+ // Helper function to get TTL statuses for keys
68
+ const getLockStatuses = async (client, keys) => {
69
+ return Promise.all(keys.map((key) => client.pttl(key)));
70
+ };
71
+ // Helper function to build lock objects from keys and statuses
72
+ const buildLockObjects = (keys, statuses, prefixLock) => {
73
+ return keys.map((key, index) => {
74
+ const ttl = statuses[index];
75
+ const exists = typeof ttl === 'number' && ttl > 0;
76
+ return {
77
+ key: key.replace(prefixLock, ''),
78
+ ttl: exists ? ttl : undefined,
79
+ expires: exists ? new Date(Date.now() + ttl).toISOString() : undefined,
80
+ };
81
+ });
82
+ };
83
+ // Helper function to calculate lock metrics
84
+ const calculateLockMetrics = async (client, prefixLock) => {
85
+ const metricsKeys = [
86
+ `${prefixLock}${METRICS_KEYS.attempts}`,
87
+ `${prefixLock}${METRICS_KEYS.acquired}`,
88
+ `${prefixLock}${METRICS_KEYS.collisions}`,
89
+ ];
90
+ const [attemptsRaw, acquiredRaw, collisionsRaw] = await client.mget(...metricsKeys);
91
+ const parseMetric = (value) => Number.isFinite(Number(value)) ? Number(value) : 0;
92
+ const attempts = parseMetric(attemptsRaw);
93
+ const acquired = parseMetric(acquiredRaw);
94
+ const collisions = parseMetric(collisionsRaw);
95
+ const collisionRate = attempts > 0 ? collisions / attempts : 0;
96
+ return { attempts, acquired, collisions, collisionRate };
97
+ };
98
+ // Helper function to build histogram from locks
99
+ const buildLockHistogram = (locks) => {
100
+ const histogram = HISTOGRAM_BUCKETS.map((bucket) => ({
101
+ label: bucket.label,
102
+ count: 0,
103
+ }));
104
+ locks.forEach((lock) => {
105
+ if (typeof lock.ttl !== 'number')
106
+ return;
107
+ const ttl = lock.ttl;
108
+ const idx = HISTOGRAM_BUCKETS.findIndex((bucket) => {
109
+ if (typeof bucket.min === 'number')
110
+ return ttl >= bucket.min;
111
+ if (typeof bucket.max === 'number')
112
+ return ttl < bucket.max;
113
+ return false;
114
+ });
115
+ if (idx >= 0)
116
+ histogram[idx].count += 1;
117
+ });
118
+ return histogram;
119
+ };
120
+ function createGetLocks(redisConfig) {
121
+ return async (pattern = '*') => {
122
+ const client = createRedisConnection(redisConfig, 3, { subsystem: 'queue-monitor-locks' });
123
+ const prefix_lock = resolveLockPrefix();
124
+ const searchPattern = `${prefix_lock}${pattern}`;
125
+ try {
126
+ // Scan for lock keys
127
+ const keys = await scanLockKeys(client, searchPattern, MAX_LOCK_KEYS);
128
+ // Get TTL statuses
129
+ const statuses = await getLockStatuses(client, keys);
130
+ // Build lock objects
131
+ const locks = buildLockObjects(keys, statuses, prefix_lock);
132
+ // Calculate metrics
133
+ const metrics = await calculateLockMetrics(client, prefix_lock);
134
+ // Build histogram
135
+ const histogram = buildLockHistogram(locks);
136
+ return {
137
+ locks,
138
+ metrics: {
139
+ active: locks.length,
140
+ ...metrics,
141
+ },
142
+ histogram,
143
+ };
144
+ }
145
+ finally {
146
+ if (typeof client.quit === 'function') {
147
+ await client.quit();
148
+ }
149
+ else if (typeof client.disconnect === 'function') {
150
+ client.disconnect();
151
+ }
152
+ }
153
+ };
154
+ }
155
+ function buildSettings(config) {
156
+ return {
157
+ enabled: config.enabled ?? DEFAULTS.enabled,
158
+ basePath: config.basePath ?? DEFAULTS.basePath,
159
+ middleware: config.middleware ?? DEFAULTS.middleware,
160
+ autoRefresh: config.autoRefresh ?? DEFAULTS.autoRefresh,
161
+ refreshIntervalMs: typeof config.refreshIntervalMs === 'number' && Number.isFinite(config.refreshIntervalMs)
162
+ ? Math.max(1000, Math.floor(config.refreshIntervalMs))
163
+ : DEFAULTS.refreshIntervalMs,
164
+ };
165
+ }
166
+ function createGetSnapshot(driver, startedAt, knownQueues) {
167
+ return async () => {
168
+ const [discoveredQueues, persistedQueues] = await Promise.all([
169
+ driver.getQueues(),
170
+ resolveKnownQueues(knownQueues),
171
+ ]);
172
+ const queues = Array.from(new Set([...persistedQueues, ...discoveredQueues])).sort((left, right) => left.localeCompare(right));
173
+ const stats = await Promise.all(queues.map(async (name) => {
174
+ const counts = await driver.getJobCounts(name);
175
+ return { name, counts: counts };
176
+ }));
177
+ return {
178
+ status: 'ok',
179
+ startedAt,
180
+ queues: stats,
181
+ };
182
+ };
183
+ }
184
+ export const QueueMonitor = Object.freeze({
185
+ create(config) {
186
+ const settings = buildSettings(config);
187
+ let redisConfig;
188
+ if (config?.redis) {
189
+ redisConfig = config?.redis;
190
+ }
191
+ else {
192
+ redisConfig = {
193
+ host: queueConfig.drivers.redis.host,
194
+ port: queueConfig.drivers.redis.port,
195
+ password: queueConfig.drivers.redis.password ?? '',
196
+ db: queueConfig.drivers.redis.database,
197
+ };
198
+ }
199
+ const driver = createBullMQDriver(redisConfig);
200
+ const metrics = createMetrics(redisConfig);
201
+ const startedAt = new Date().toISOString();
202
+ ShutdownTrace.logHandles('queue-monitor.create', {
203
+ basePath: settings.basePath,
204
+ autoRefresh: settings.autoRefresh,
205
+ refreshIntervalMs: settings.refreshIntervalMs,
206
+ });
207
+ const getSnapshot = createGetSnapshot(driver, startedAt, config.knownQueues);
208
+ const getLocks = createGetLocks(redisConfig);
209
+ const close = async () => {
210
+ ShutdownTrace.logHandles('queue-monitor.close.start', {
211
+ basePath: settings.basePath,
212
+ });
213
+ await Promise.all([driver.close(), metrics.close()]);
214
+ ShutdownTrace.logHandles('queue-monitor.close.complete', {
215
+ basePath: settings.basePath,
216
+ });
217
+ };
218
+ return Object.freeze({
219
+ getSnapshot,
220
+ getLocks,
221
+ driver,
222
+ metrics,
223
+ close,
224
+ });
225
+ },
226
+ });
227
+ export default QueueMonitor;
228
+ export { createBullMQDriver } from './driver';
229
+ /**
230
+ * Package version and build metadata
231
+ * Available at runtime for debugging and health checks
232
+ */
233
+ export const _ZINTRUST_QUEUE_MONITOR_VERSION = '0.1.0';
234
+ export const _ZINTRUST_QUEUE_MONITOR_BUILD_DATE = '__BUILD_DATE__';
@@ -0,0 +1,7 @@
1
+ export type WorkerUiOptions = {
2
+ basePath: string;
3
+ apiBaseUrl?: string;
4
+ autoRefresh: boolean;
5
+ refreshIntervalMs: number;
6
+ };
7
+ export declare const getWorkersHtml: (options: WorkerUiOptions) => string;