@zintrust/queue-monitor 0.4.48 → 0.4.50

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.
@@ -2,6 +2,7 @@ import type { IRequest, IResponse } from '@zintrust/core';
2
2
  import type { QueueDriver } from './driver';
3
3
  import type { LockAnalytics, QueueMonitorSnapshot } from './index';
4
4
  import type { JobSummary, Metrics } from './metrics';
5
+ export declare const ALL_QUEUES = "__all__";
5
6
  type QueueSnapshotData = {
6
7
  type: string;
7
8
  ts: string;
@@ -20,11 +21,11 @@ type QueueMonitoringConfig = {
20
21
  pattern: string;
21
22
  intervalMs: number;
22
23
  };
24
+ type QueueMonitoringCallback = (data: QueueSnapshotData) => void;
25
+ export declare function getRecentJobsForSelection(queueName: string, metrics: Metrics, driver: QueueDriver, queueNames?: ReadonlyArray<string>): Promise<JobSummary[]>;
23
26
  export declare const QueueMonitoringService: Readonly<{
24
- subscribe(callback: (data: QueueSnapshotData) => void): void;
25
- unsubscribe(callback: (data: QueueSnapshotData) => void): void;
26
- startPollingForClient(config: QueueMonitoringConfig): void;
27
- stopPollingForClient(): void;
27
+ subscribe(callback: QueueMonitoringCallback, config: QueueMonitoringConfig): void;
28
+ unsubscribe(callback: QueueMonitoringCallback): void;
28
29
  }>;
29
30
  export declare const QueueMonitoringStream: (res: IResponse, req: IRequest, getSnapshot: () => Promise<QueueMonitorSnapshot>, getLocks: (pattern?: string) => Promise<LockAnalytics>, metrics: Metrics, driver: QueueDriver, settings: {
30
31
  basePath: string;
@@ -1,80 +1,92 @@
1
- import { Logger, NodeSingletons } from '@zintrust/core';
2
- // Internal state
3
- const emitter = new NodeSingletons.EventEmitter();
4
- emitter.setMaxListeners(Infinity);
5
- let interval = null;
6
- let subscribers = 0;
7
- let currentConfig = null;
8
- const broadcastSnapshot = async () => {
1
+ import { Logger } from '@zintrust/core';
2
+ export const ALL_QUEUES = '__all__';
3
+ const subscriptions = new Map();
4
+ const isAllQueuesSelection = (queue) => queue === ALL_QUEUES;
5
+ const sortJobsByTimestamp = (jobs) => jobs.sort((left, right) => right.timestamp - left.timestamp);
6
+ export async function getRecentJobsForSelection(queueName, metrics, driver, queueNames) {
7
+ if (!isAllQueuesSelection(queueName)) {
8
+ return getRecentJobsForQueue(queueName, metrics, driver);
9
+ }
10
+ const names = Array.from(new Set((queueNames ?? (await driver.getQueues())).filter(Boolean)));
11
+ const jobsByQueue = await Promise.all(names.map(async (name) => getRecentJobsForQueue(name, metrics, driver)));
12
+ return sortJobsByTimestamp(jobsByQueue.flat()).slice(0, 100);
13
+ }
14
+ const buildSnapshotPayload = async (config) => {
15
+ const { getSnapshot, getLocks, metrics, driver, queue: configuredQueue, pattern } = config;
16
+ const snapshot = await getSnapshot();
17
+ let queue;
18
+ if (isAllQueuesSelection(configuredQueue)) {
19
+ queue = ALL_QUEUES;
20
+ }
21
+ else if (configuredQueue &&
22
+ snapshot.queues.some((candidate) => candidate.name === configuredQueue)) {
23
+ queue = configuredQueue;
24
+ }
25
+ else {
26
+ queue = snapshot.queues[0]?.name ?? null;
27
+ }
28
+ return {
29
+ type: 'snapshot',
30
+ ts: new Date().toISOString(),
31
+ queue,
32
+ snapshot,
33
+ jobs: queue
34
+ ? await getRecentJobsForSelection(queue, metrics, driver, snapshot.queues.map((candidate) => candidate.name))
35
+ : [],
36
+ locks: await getLocks(pattern),
37
+ };
38
+ };
39
+ const pushSnapshot = async (subscription) => {
9
40
  try {
10
- if (subscribers <= 0 || !currentConfig)
11
- return;
12
- const { getSnapshot, getLocks, metrics, driver, queue: initialQueue, pattern } = currentConfig;
13
- const snapshot = await getSnapshot();
14
- let queue = initialQueue;
15
- if (!queue && snapshot.queues.length > 0) {
16
- queue = snapshot.queues[0].name;
17
- }
18
- const jobs = queue ? await getRecentJobsForQueue(queue, metrics, driver) : [];
19
- const locks = await getLocks(pattern);
20
- const payload = {
21
- type: 'snapshot',
22
- ts: new Date().toISOString(),
23
- queue: queue || null,
24
- snapshot,
25
- jobs,
26
- locks,
27
- };
28
- emitter.emit('snapshot', payload);
41
+ subscription.callback(await buildSnapshotPayload(subscription.config));
29
42
  }
30
43
  catch (err) {
31
- Logger.error('QueueMonitoringService.broadcastSnapshot failed', err);
32
- if (emitter.listenerCount('error') > 0) {
33
- emitter.emit('error', err);
34
- }
44
+ Logger.error('QueueMonitoringService.pushSnapshot failed', err);
35
45
  }
36
46
  };
37
- const startPolling = () => {
38
- if (interval || !currentConfig)
47
+ const startPolling = (subscription) => {
48
+ if (subscription.interval)
39
49
  return;
40
- Logger.debug('Starting QueueMonitoringService polling');
41
- // Initial fetch
42
- void broadcastSnapshot();
43
- interval = setInterval(() => {
44
- void broadcastSnapshot();
45
- }, currentConfig.intervalMs);
50
+ Logger.debug('Starting QueueMonitoringService polling', {
51
+ queue: subscription.config.queue || null,
52
+ pattern: subscription.config.pattern,
53
+ });
54
+ void pushSnapshot(subscription);
55
+ subscription.interval = setInterval(() => {
56
+ void pushSnapshot(subscription);
57
+ }, subscription.config.intervalMs);
46
58
  };
47
- const stopPolling = () => {
48
- if (interval) {
49
- Logger.debug('Stopping QueueMonitoringService polling');
50
- clearInterval(interval);
51
- interval = null;
52
- }
59
+ const stopPolling = (subscription) => {
60
+ if (!subscription.interval)
61
+ return;
62
+ Logger.debug('Stopping QueueMonitoringService polling', {
63
+ queue: subscription.config.queue || null,
64
+ pattern: subscription.config.pattern,
65
+ });
66
+ clearInterval(subscription.interval);
67
+ subscription.interval = null;
53
68
  };
54
69
  export const QueueMonitoringService = Object.freeze({
55
- subscribe(callback) {
56
- emitter.on('snapshot', callback);
57
- subscribers++;
58
- },
59
- unsubscribe(callback) {
60
- emitter.off('snapshot', callback);
61
- subscribers--;
62
- if (subscribers <= 0) {
63
- stopPolling();
64
- currentConfig = null;
65
- }
66
- },
67
- startPollingForClient(config) {
68
- if (subscribers === 1) {
69
- currentConfig = config;
70
- startPolling();
70
+ subscribe(callback, config) {
71
+ const existing = subscriptions.get(callback);
72
+ if (existing) {
73
+ stopPolling(existing);
74
+ subscriptions.delete(callback);
71
75
  }
76
+ const subscription = {
77
+ callback,
78
+ config,
79
+ interval: null,
80
+ };
81
+ subscriptions.set(callback, subscription);
82
+ startPolling(subscription);
72
83
  },
73
- stopPollingForClient() {
74
- if (subscribers <= 0) {
75
- stopPolling();
76
- currentConfig = null;
77
- }
84
+ unsubscribe(callback) {
85
+ const subscription = subscriptions.get(callback);
86
+ if (!subscription)
87
+ return;
88
+ stopPolling(subscription);
89
+ subscriptions.delete(callback);
78
90
  },
79
91
  });
80
92
  // settings: {
@@ -116,10 +128,7 @@ export const QueueMonitoringStream = (res, req, getSnapshot, getLocks, metrics,
116
128
  const onSnapshot = (data) => {
117
129
  send(data);
118
130
  };
119
- // Subscribe to centralized service
120
- QueueMonitoringService.subscribe(onSnapshot);
121
- // Start polling for this client
122
- QueueMonitoringService.startPollingForClient({
131
+ QueueMonitoringService.subscribe(onSnapshot, {
123
132
  getSnapshot,
124
133
  getLocks,
125
134
  getRecentJobsForQueue,
@@ -138,13 +147,15 @@ export const QueueMonitoringStream = (res, req, getSnapshot, getLocks, metrics,
138
147
  closed = true;
139
148
  clearInterval(hb);
140
149
  QueueMonitoringService.unsubscribe(onSnapshot);
141
- QueueMonitoringService.stopPollingForClient();
142
150
  });
143
151
  };
144
152
  export async function getRecentJobsForQueue(queueName, metrics, driver) {
145
153
  const recent = await metrics.getRecentJobs(queueName);
146
154
  const failed = await metrics.getFailedJobs(queueName);
147
- const all = [...recent, ...failed].sort((a, b) => b.timestamp - a.timestamp).slice(0, 100);
155
+ const all = sortJobsByTimestamp([...recent, ...failed].map((job) => ({
156
+ ...job,
157
+ queue: job.queue ?? queueName,
158
+ }))).slice(0, 100);
148
159
  if (all.length > 0) {
149
160
  return all;
150
161
  }
@@ -182,6 +193,7 @@ export async function getRecentJobsForQueue(queueName, metrics, driver) {
182
193
  return {
183
194
  id: job.id,
184
195
  name: job.name,
196
+ queue: queueName,
185
197
  data: job.data,
186
198
  attempts: job.attemptsMade,
187
199
  status,
@@ -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": "0.4.48",
4
- "buildDate": "2026-04-03T09:57:53.962Z",
3
+ "version": "0.4.43",
4
+ "buildDate": "2026-04-01T18:15:53.560Z",
5
5
  "buildEnvironment": {
6
- "node": "v20.20.1",
7
- "platform": "linux",
8
- "arch": "x64"
6
+ "node": "v22.22.1",
7
+ "platform": "darwin",
8
+ "arch": "arm64"
9
9
  },
10
10
  "git": {
11
- "commit": "0ac23637",
12
- "branch": "master"
11
+ "commit": "57e4d1b5",
12
+ "branch": "release"
13
13
  },
14
14
  "package": {
15
15
  "engines": {
@@ -32,6 +32,34 @@
32
32
  "size": 6190,
33
33
  "sha256": "068477c0b545686c9c35d6b42960b5ecf8d19ee4f3f5adec8ddeeb21113258b3"
34
34
  },
35
+ "api/workerClient.d.ts": {
36
+ "size": 597,
37
+ "sha256": "1d712bfa9127aa2df4f1fbd0efbb6d84069e1888ed73aca5542a41eee865d4bb"
38
+ },
39
+ "api/workerClient.js": {
40
+ "size": 1629,
41
+ "sha256": "60f993a42f4a9dd5000b01dae3b0f105a8f4348da8433a735b5cc1929a8f64ce"
42
+ },
43
+ "build-manifest.json": {
44
+ "size": 3865,
45
+ "sha256": "6b9eece186e37f5ed657a014c799e3783f30d0e16e0c2fb4e9fa8f2ffcbdd673"
46
+ },
47
+ "config/queueMonitor.d.ts": {
48
+ "size": 407,
49
+ "sha256": "4541f47e64c8ede1bfd8fc0cb7edb76c4e885311b28b1f51c9be7639e5d87eca"
50
+ },
51
+ "config/queueMonitor.js": {
52
+ "size": 689,
53
+ "sha256": "0b95e6b65d4b6ffdd69788cdfd19e0e76400a39ad69dce018d8827a3b298e419"
54
+ },
55
+ "config/workerConfig.d.ts": {
56
+ "size": 86,
57
+ "sha256": "b669205d50c8844455a2d9b34a54f48a71118eb6ac99bad5372683ab666f5a22"
58
+ },
59
+ "config/workerConfig.js": {
60
+ "size": 628,
61
+ "sha256": "ca1c6dbaa751893f0e6b7c8a7fd41a80f7d5e8fc9aaaa4877ca12821bb25f56f"
62
+ },
35
63
  "connection.d.ts": {
36
64
  "size": 107,
37
65
  "sha256": "653b300a25df08a2380bdc74ea38342190771386cb1847dc92802e6eef88a88f"
@@ -62,7 +90,7 @@
62
90
  },
63
91
  "index.js": {
64
92
  "size": 11889,
65
- "sha256": "89c18a0da652cf8a7502b5c552250e419c66edb03c7d6ca358b54278d5cf53a9"
93
+ "sha256": "19a7bdc71cec34fe732dd0d7177ad366d271e31462f402911d4a2700d0ea8b2c"
66
94
  },
67
95
  "metrics.d.ts": {
68
96
  "size": 848,
@@ -72,6 +100,14 @@
72
100
  "size": 3448,
73
101
  "sha256": "022c97865d37933fd7ed92f47b86b0450056caffd629fce6add51c902529bfe5"
74
102
  },
103
+ "routes/workers.d.ts": {
104
+ "size": 477,
105
+ "sha256": "cfcc3527c47d1a796a3ced2d4777b339dd767f7feb3ecc66c5ce0a91d8ff1bc8"
106
+ },
107
+ "routes/workers.js": {
108
+ "size": 901,
109
+ "sha256": "1ec48c0becc8c0628b1780fb400e957c5fe50da79f73112436c33ecde7d7d574"
110
+ },
75
111
  "worker.d.ts": {
76
112
  "size": 332,
77
113
  "sha256": "97cddbc991c6e6724950090cd92f06671932ab848c16dd94a030d9fba3fbae26"
@@ -79,6 +115,14 @@
79
115
  "worker.js": {
80
116
  "size": 1233,
81
117
  "sha256": "da26c1b80da7473e1644a4e0f8d814097ce0adff14e36037164f9ae5855e656a"
118
+ },
119
+ "workers-ui.d.ts": {
120
+ "size": 214,
121
+ "sha256": "4f5fd3bbd077d0dee2cc6c73d2c736fc52119b0d3a5f4a1878a4f3d3d3edf299"
122
+ },
123
+ "workers-ui.js": {
124
+ "size": 24928,
125
+ "sha256": "ba2c5cbf890fe875cc5336c76bd29b146089bc8ba3a70de4a2c383f395f9be73"
82
126
  }
83
127
  }
84
128
  }
@@ -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
+ });