@zintrust/queue-monitor 0.5.8 → 0.7.7

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.
@@ -259,6 +259,7 @@ export async function getRecentJobsForQueue(queueName, metrics, driver) {
259
259
  name: job.name,
260
260
  queue: queueName,
261
261
  data: job.data,
262
+ opts: job.opts,
262
263
  attempts: job.attemptsMade,
263
264
  status,
264
265
  failedReason: job.failedReason || undefined,
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "@zintrust/queue-monitor",
3
- "version": "0.5.8",
4
- "buildDate": "2026-04-14T16:59:05.681Z",
3
+ "version": "0.7.7",
4
+ "buildDate": "2026-04-19T14:20:25.049Z",
5
5
  "buildEnvironment": {
6
6
  "node": "v20.20.2",
7
7
  "platform": "linux",
8
8
  "arch": "x64"
9
9
  },
10
10
  "git": {
11
- "commit": "8d52a13d",
11
+ "commit": "3921a3ee",
12
12
  "branch": "master"
13
13
  },
14
14
  "package": {
@@ -37,8 +37,8 @@
37
37
  "sha256": "26a91f1b41d8a976a9e8fabaedc3fe6dc955c700499f0afc1a16fb91c9848665"
38
38
  },
39
39
  "QueueMonitoringService.js": {
40
- "size": 8900,
41
- "sha256": "0d00a27755a372ce77bb5921ce25fe024d3e57274cd48f963457f76bf5486e4b"
40
+ "size": 8928,
41
+ "sha256": "fc408db720d5883821b169eabce66944295068cd6f2338f1884493c299f0a0f1"
42
42
  },
43
43
  "connection.d.ts": {
44
44
  "size": 107,
@@ -53,32 +53,32 @@
53
53
  "sha256": "5140191207b5620500ac5e1edacefed65956a1ad88739695d545ea7d2b405243"
54
54
  },
55
55
  "dashboard-ui.js": {
56
- "size": 54386,
57
- "sha256": "9b8d8c41cefbb7a153956c0ed3c29b5fa2c7aa10077b4ca81ae5a99621cfbce1"
56
+ "size": 54576,
57
+ "sha256": "b3ee7f13f8fa2fee8204316ac5fe4c9ee0d424131565a58de17061296c7c8e1b"
58
58
  },
59
59
  "driver.d.ts": {
60
- "size": 707,
61
- "sha256": "b672414002f819d195cd97d7eb35fee1c09324e33e713e443f524323f65bea28"
60
+ "size": 1102,
61
+ "sha256": "70b74e7a0489da3a5545dfd8458ab5b4def1f3733c291a782c033fed0afd0bcf"
62
62
  },
63
63
  "driver.js": {
64
- "size": 4105,
65
- "sha256": "50229d05df9ca8bf5528b923e2799a4719e5a32ad9fa30bf3e2f36ecc91cf00c"
64
+ "size": 5165,
65
+ "sha256": "06a4aa4294ed0694146d184e8b0f1677ebe22a3d90bffa30b7c268657035c760"
66
66
  },
67
67
  "index.d.ts": {
68
68
  "size": 2161,
69
69
  "sha256": "2556c136641769f4e7cd81abd6ecae8663433aba74b09bc507b8fba01e3fc6f5"
70
70
  },
71
71
  "index.js": {
72
- "size": 12869,
73
- "sha256": "8d0f8ad669b74d5152768e9265cccd4c79320a0046f3d3c26d00bd802077874b"
72
+ "size": 13905,
73
+ "sha256": "707429961a88fe448506c3d3ca5c348cfa9305addd10bc081b1191ff141e8218"
74
74
  },
75
75
  "metrics.d.ts": {
76
- "size": 868,
77
- "sha256": "cbae21100c770e0960f4ae8c96bfaa2015162042122e04f00812817f04306512"
76
+ "size": 910,
77
+ "sha256": "6467795f6de818a1f295c42755005e394f25b145163c7678d6ac9ac1f9477fea"
78
78
  },
79
79
  "metrics.js": {
80
- "size": 3448,
81
- "sha256": "022c97865d37933fd7ed92f47b86b0450056caffd629fce6add51c902529bfe5"
80
+ "size": 3472,
81
+ "sha256": "fab0e6d62ed694dcf57904a68b683280e4063c2fdcf97c60fdf5d933ae2b8f4f"
82
82
  },
83
83
  "worker.d.ts": {
84
84
  "size": 332,
@@ -983,9 +983,12 @@ const getRetryJobFunction = () => `
983
983
  const res = await fetch(API_BASE + '/api/retry/' + queueName + '/' + jobId, {
984
984
  method: 'POST'
985
985
  });
986
+ const payload = await res.json().catch(() => null);
986
987
 
987
988
  if (res.ok) {
988
- btn.textContent = '✓ Retried';
989
+ btn.textContent = payload && payload.status === 'requeued_from_snapshot'
990
+ ? '✓ Requeued'
991
+ : '✓ Retried';
989
992
  setTimeout(() => {
990
993
  console.log('HTTP jobs polling disabled - using SSE only');
991
994
  // fetchJobs(currentQueue);
package/dist/driver.d.ts CHANGED
@@ -2,12 +2,32 @@ import type { Job, JobsOptions } from 'bullmq';
2
2
  import { type RedisConfig } from './connection';
3
3
  export type JobPayload<T = unknown> = T;
4
4
  export type JobCounts = Record<string, number>;
5
+ export type RetrySnapshot = {
6
+ name?: string;
7
+ data: unknown;
8
+ opts?: JobsOptions;
9
+ };
10
+ export type RetryJobResult = {
11
+ ok: true;
12
+ status: 'retried';
13
+ } | {
14
+ ok: true;
15
+ status: 'requeued_from_snapshot';
16
+ newJobId?: string;
17
+ } | {
18
+ ok: false;
19
+ status: 'missing';
20
+ } | {
21
+ ok: false;
22
+ status: 'not_retryable';
23
+ reason?: string;
24
+ };
5
25
  export type QueueDriver = {
6
26
  enqueue<T>(name: string, payload: T, options?: JobsOptions): Promise<string>;
7
27
  getJob(queueName: string, jobId: string): Promise<Job | undefined>;
8
28
  getJobCounts(queueName: string): Promise<JobCounts>;
9
29
  getRecentJobs(queueName: string, limit?: number): Promise<Job[]>;
10
- retryJob(queueName: string, jobId: string): Promise<boolean>;
30
+ retryJob(queueName: string, jobId: string, snapshot?: RetrySnapshot): Promise<RetryJobResult>;
11
31
  getQueues(): Promise<string[]>;
12
32
  close(): Promise<void>;
13
33
  };
package/dist/driver.js CHANGED
@@ -41,6 +41,7 @@ async function discoverQueuesFromRedis(redis, inMemoryQueues) {
41
41
  }
42
42
  return Array.from(found.values());
43
43
  }
44
+ // eslint-disable-next-line max-lines-per-function
44
45
  export const createBullMQDriver = (config) => {
45
46
  const queues = new Map();
46
47
  const redis = createRedisConnection(config, 3, { subsystem: 'queue-monitor' });
@@ -77,6 +78,23 @@ export const createBullMQDriver = (config) => {
77
78
  const queue = getQueue(queueName);
78
79
  return queue.getJobCounts();
79
80
  };
81
+ const requeueFromSnapshot = async (queue, snapshot) => {
82
+ try {
83
+ const requeued = await queue.add(snapshot.name ?? 'default', snapshot.data, snapshot.opts);
84
+ return {
85
+ ok: true,
86
+ status: 'requeued_from_snapshot',
87
+ newJobId: requeued.id === undefined || requeued.id === null ? undefined : String(requeued.id),
88
+ };
89
+ }
90
+ catch (error) {
91
+ return {
92
+ ok: false,
93
+ status: 'not_retryable',
94
+ reason: error instanceof Error ? error.message : String(error),
95
+ };
96
+ }
97
+ };
80
98
  const getRecentJobs = async (queueName, limit = 100) => {
81
99
  const queue = getQueue(queueName);
82
100
  const jobs = await queue.getJobs(['completed', 'failed', 'active', 'waiting', 'delayed', 'paused'], 0, Math.max(0, limit - 1), true);
@@ -84,16 +102,24 @@ export const createBullMQDriver = (config) => {
84
102
  await enrichJobsWithState(jobs);
85
103
  return jobs;
86
104
  };
87
- const retryJob = async (queueName, jobId) => {
105
+ const retryJob = async (queueName, jobId, snapshot) => {
106
+ const queue = getQueue(queueName);
88
107
  const job = await getJob(queueName, jobId);
89
- if (!job)
90
- return false;
108
+ if (!job) {
109
+ if (snapshot)
110
+ return requeueFromSnapshot(queue, snapshot);
111
+ return { ok: false, status: 'missing' };
112
+ }
91
113
  try {
92
114
  await job.retry();
93
- return true;
115
+ return { ok: true, status: 'retried' };
94
116
  }
95
- catch {
96
- return false;
117
+ catch (error) {
118
+ return {
119
+ ok: false,
120
+ status: 'not_retryable',
121
+ reason: error instanceof Error ? error.message : String(error),
122
+ };
97
123
  }
98
124
  };
99
125
  const getQueues = async () => {
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { Env, isArray, isNonEmptyString, Logger, queueConfig, resolveLockPrefix, Router, } from '@zintrust/core';
1
+ import { Env, isNonEmptyString, Logger, queueConfig, resolveLockPrefix, Router, } from '@zintrust/core';
2
2
  import { createRedisConnection } from './connection';
3
3
  import { getDashboardHtml } from './dashboard-ui';
4
4
  import { createBullMQDriver } from './driver';
@@ -37,8 +37,7 @@ const HISTOGRAM_BUCKETS = [
37
37
  const MAX_LOCK_KEYS = 10_000;
38
38
  function normalizeQueueNames(queueNames) {
39
39
  return Array.from(new Set(queueNames
40
- .map((queueName) => queueName)
41
- .filter(isNonEmptyString)
40
+ .filter((queueName) => typeof queueName === 'string' && isNonEmptyString(queueName))
42
41
  .map((name) => name.trim())))
43
42
  .filter((name) => name.length > 0)
44
43
  .sort((left, right) => left.localeCompare(right));
@@ -47,7 +46,7 @@ async function resolveKnownQueues(knownQueues) {
47
46
  if (typeof knownQueues === 'function') {
48
47
  return normalizeQueueNames(await knownQueues());
49
48
  }
50
- if (isArray(knownQueues)) {
49
+ if (Array.isArray(knownQueues)) {
51
50
  return normalizeQueueNames(knownQueues);
52
51
  }
53
52
  return [];
@@ -169,20 +168,44 @@ async function handleJobsEndpoint(req, res, metrics, driver) {
169
168
  const jobs = await getRecentJobsForSelection(queueName, metrics, driver);
170
169
  res.json(jobs);
171
170
  }
172
- async function handleRetryEndpoint(req, res, driver) {
171
+ async function handleRetryEndpoint(req, res, driver, metrics) {
173
172
  const queueName = extractQueueParam(req);
174
173
  const jobId = typeof req.getParam === 'function' ? req.getParam?.('jobId') : req.params?.['jobId'];
175
174
  if (!queueName || !jobId) {
176
175
  res.status(400).json(fieldError('queue_name,job_id', 'Queue name and job ID must be provided'));
177
176
  return;
178
177
  }
179
- const success = await driver.retryJob(queueName, jobId);
180
- if (success) {
181
- res.json({ ok: true, message: `Job ${jobId} queued for retry` });
178
+ const recentJobs = await getRecentJobsForSelection(queueName, metrics, driver);
179
+ const snapshotCandidate = recentJobs.find((job) => String(job.id) === jobId);
180
+ const retrySnapshot = snapshotCandidate === undefined
181
+ ? undefined
182
+ : {
183
+ name: snapshotCandidate.name,
184
+ data: snapshotCandidate.data,
185
+ opts: snapshotCandidate.opts,
186
+ };
187
+ const result = await driver.retryJob(queueName, jobId, retrySnapshot);
188
+ if (result.ok) {
189
+ res.json({
190
+ ok: true,
191
+ status: result.status,
192
+ message: result.status === 'requeued_from_snapshot'
193
+ ? `Job ${jobId} re-queued from monitor snapshot`
194
+ : `Job ${jobId} queued for retry`,
195
+ ...(result.status === 'requeued_from_snapshot' && result.newJobId
196
+ ? { newJobId: result.newJobId }
197
+ : {}),
198
+ });
199
+ return;
182
200
  }
183
- else {
184
- res.status(404).json({ error: 'Job not found or cannot be retried' });
201
+ if (result.status === 'missing') {
202
+ res.status(404).json({ error: `Job ${jobId} no longer exists`, status: result.status });
203
+ return;
185
204
  }
205
+ res.status(409).json({
206
+ error: result.reason ?? `Job ${jobId} cannot be retried in its current state`,
207
+ status: result.status,
208
+ });
186
209
  }
187
210
  function buildSettings(config) {
188
211
  return {
@@ -241,7 +264,7 @@ function registerApiRoutes(router, settings, routeOptions, metrics, driver, getS
241
264
  registerSnapshotApi(router, settings, routeOptions, getSnapshot);
242
265
  registerJobsApi(router, settings, routeOptions, metrics, driver);
243
266
  registerLocksApi(router, settings, routeOptions, getLocks);
244
- registerRetryApi(router, settings, routeOptions, driver);
267
+ registerRetryApi(router, settings, routeOptions, driver, metrics);
245
268
  registerEventsApi(router, settings, routeOptions, getSnapshot, getLocks, metrics, driver);
246
269
  }
247
270
  function registerSnapshotApi(router, settings, routeOptions, getSnapshot) {
@@ -265,9 +288,9 @@ function registerLocksApi(router, settings, routeOptions, getLocks) {
265
288
  res.json(locks);
266
289
  }, routeOptions);
267
290
  }
268
- function registerRetryApi(router, settings, routeOptions, driver) {
291
+ function registerRetryApi(router, settings, routeOptions, driver, metrics) {
269
292
  Router.post(router, `${settings.basePath}/api/retry/:queue/:jobId`, async (req, res) => {
270
- await handleRetryEndpoint(req, res, driver);
293
+ await handleRetryEndpoint(req, res, driver, metrics);
271
294
  }, routeOptions);
272
295
  }
273
296
  function registerEventsApi(router, settings, routeOptions, getSnapshot, getLocks, metrics, driver) {
package/dist/metrics.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type Job } from 'bullmq';
1
+ import { type Job, type JobsOptions } from 'bullmq';
2
2
  import { type RedisConfig } from './connection';
3
3
  export type JobStatus = 'completed' | 'failed';
4
4
  export type JobSummary = {
@@ -6,6 +6,7 @@ export type JobSummary = {
6
6
  name: string;
7
7
  queue?: string;
8
8
  data: unknown;
9
+ opts?: JobsOptions;
9
10
  attempts: number;
10
11
  status?: string;
11
12
  failedReason?: string;
package/dist/metrics.js CHANGED
@@ -19,6 +19,7 @@ const recordJobImpl = async (redis, queue, status, job, error) => {
19
19
  id: job.id,
20
20
  name: job.name,
21
21
  data: job.data,
22
+ opts: job.opts,
22
23
  attempts: job.attemptsMade,
23
24
  failedReason: job.failedReason || error?.message,
24
25
  timestamp: Date.now(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zintrust/queue-monitor",
3
- "version": "0.5.8",
3
+ "version": "0.7.7",
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.5.7"
23
+ "@zintrust/core": "^0.7.7"
24
24
  },
25
25
  "publishConfig": {
26
26
  "access": "public"