@zintrust/workers 0.4.50 → 0.4.60

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.
@@ -23,6 +23,7 @@ import { WorkerMetrics } from './WorkerMetrics.js';
23
23
  import { WorkerRegistry } from './WorkerRegistry.js';
24
24
  import { WorkerVersioning } from './WorkerVersioning.js';
25
25
  import { keyPrefix } from './config/workerConfig.js';
26
+ import { recordQueueMonitorJob } from './queueMonitorHistory.js';
26
27
  import { DbWorkerStore, InMemoryWorkerStore, RedisWorkerStore, } from './storage/WorkerStore.js';
27
28
  const path = NodeSingletons.path;
28
29
  const isNodeRuntime = () => typeof process !== 'undefined' && Boolean(process.versions?.node);
@@ -1905,10 +1906,15 @@ const initializeDatacenter = (config) => {
1905
1906
  },
1906
1907
  });
1907
1908
  };
1908
- const setupWorkerEventListeners = (worker, workerName, workerVersion, features) => {
1909
+ const setupWorkerEventListeners = (worker, workerName, queueName, workerVersion, features) => {
1909
1910
  worker.on('completed', (job) => {
1910
1911
  try {
1911
1912
  Logger.debug(`Job completed: ${workerName}`, { jobId: job.id });
1913
+ void recordQueueMonitorJob({
1914
+ queueName,
1915
+ status: 'completed',
1916
+ job,
1917
+ });
1912
1918
  if (features?.observability === true) {
1913
1919
  Observability.incrementCounter('worker.jobs.completed', 1, {
1914
1920
  worker: workerName,
@@ -1924,6 +1930,14 @@ const setupWorkerEventListeners = (worker, workerName, workerVersion, features)
1924
1930
  worker.on('failed', (job, error) => {
1925
1931
  try {
1926
1932
  Logger.error(`Job failed: ${workerName}`, { error, jobId: job?.id }, 'workers');
1933
+ if (job) {
1934
+ void recordQueueMonitorJob({
1935
+ queueName,
1936
+ status: 'failed',
1937
+ job,
1938
+ error,
1939
+ });
1940
+ }
1927
1941
  if (features?.observability === true) {
1928
1942
  Observability.incrementCounter('worker.jobs.failed', 1, {
1929
1943
  worker: workerName,
@@ -2087,7 +2101,7 @@ export const WorkerFactory = Object.freeze({
2087
2101
  // Create BullMQ worker
2088
2102
  const resolvedOptions = resolveWorkerOptions(config, autoStart);
2089
2103
  const worker = new Worker(queueName, enhancedProcessor, resolvedOptions);
2090
- setupWorkerEventListeners(worker, name, workerVersion, features);
2104
+ setupWorkerEventListeners(worker, name, queueName, workerVersion, features);
2091
2105
  // Update status to "starting"
2092
2106
  await store.update(name, {
2093
2107
  status: WorkerCreationStatus.STARTING,
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "@zintrust/workers",
3
- "version": "0.4.43",
4
- "buildDate": "2026-04-01T18:18:47.850Z",
3
+ "version": "0.4.60",
4
+ "buildDate": "2026-04-05T07:03:43.939Z",
5
5
  "buildEnvironment": {
6
6
  "node": "v22.22.1",
7
7
  "platform": "darwin",
8
8
  "arch": "arm64"
9
9
  },
10
10
  "git": {
11
- "commit": "57e4d1b5",
11
+ "commit": "be058c1d",
12
12
  "branch": "release"
13
13
  },
14
14
  "package": {
@@ -178,8 +178,8 @@
178
178
  "sha256": "3869f960c87260588e40941ff91bffcfa0757be7a04815fd28b57dd4840c51df"
179
179
  },
180
180
  "WorkerFactory.js": {
181
- "size": 102863,
182
- "sha256": "1024756e603ca67461a955723dc004de9395267e50c7f59c9f8f0c13b2f0f7d8"
181
+ "size": 105839,
182
+ "sha256": "b26135593d6e3849f5540d74d097082d8186b50c72a456722f83d624141c42c8"
183
183
  },
184
184
  "WorkerInit.d.ts": {
185
185
  "size": 3284,
@@ -231,7 +231,7 @@
231
231
  },
232
232
  "build-manifest.json": {
233
233
  "size": 19594,
234
- "sha256": "ddb01f1c22cddfc1201631a2774726d0134407e811359eed517bf36b508d829c"
234
+ "sha256": "7402721e5f5a98fb988fa78f5ee49b6e82f2f3fab1c3cfb8ca2bf8116012169f"
235
235
  },
236
236
  "config/workerConfig.d.ts": {
237
237
  "size": 132,
@@ -246,8 +246,8 @@
246
246
  "sha256": "dacd49f6c112eba439bdd9bb457eea90daedbf32efc381cd3189ce562fa5b0a8"
247
247
  },
248
248
  "createQueueWorker.js": {
249
- "size": 14702,
250
- "sha256": "69bf07658c185ad5b4bafd064bfc64ea257c769809b6d4811a274020b4a5a8e7"
249
+ "size": 15594,
250
+ "sha256": "0710f2fb6936a326cdbe6fa6a2b8acfb60f7c6b27acb2aed0b98bf2402fc67f4"
251
251
  },
252
252
  "dashboard/index.d.ts": {
253
253
  "size": 109,
@@ -270,8 +270,8 @@
270
270
  "sha256": "8e0e04329e1119d8ae835dd4458efead084293bcc2c263c09dd5a19d467e5ca4"
271
271
  },
272
272
  "dashboard/workers-api.js": {
273
- "size": 28207,
274
- "sha256": "ae1ff0e962b1f64e6d200a35c3b8b1de968341d8df6283625a3efce50a23826c"
273
+ "size": 28568,
274
+ "sha256": "c28f0d4b8134337a2cc71f1ce2f1d6195f44ccd9bb0b8fa26a4edaed88ee4fc0"
275
275
  },
276
276
  "helper/index.d.ts": {
277
277
  "size": 159,
@@ -415,7 +415,15 @@
415
415
  },
416
416
  "index.js": {
417
417
  "size": 2337,
418
- "sha256": "698bb78d6613e898a60f390ca1766071324e3ec3a726184c35f174ea64cadb99"
418
+ "sha256": "85a789d0f2ed71dde649f59658827ff16eaf2c1bc77df99b2297c7eb7e340108"
419
+ },
420
+ "queueMonitorHistory.d.ts": {
421
+ "size": 433,
422
+ "sha256": "1ba25cf47b0cad83e92c7b6481d12de974b921d0e01fc4cf17fc833acdf8b8c8"
423
+ },
424
+ "queueMonitorHistory.js": {
425
+ "size": 2194,
426
+ "sha256": "03d406adc046e18c3526837a654fae93b112fb6010ddc1163b129d10e934c4d4"
419
427
  },
420
428
  "register.d.ts": {
421
429
  "size": 256,
@@ -1,5 +1,6 @@
1
1
  import * as Core from '@zintrust/core';
2
2
  import { Env, Logger, Queue } from '@zintrust/core';
3
+ import { recordQueueMonitorJob } from './queueMonitorHistory.js';
3
4
  const TypedQueue = Queue;
4
5
  const RETRY_BASE_DELAY_MS = 1000;
5
6
  const RETRY_MAX_DELAY_MS = 30000;
@@ -168,6 +169,18 @@ const checkAndRequeueIfNotDue = async (options, queueName, driverName, message,
168
169
  };
169
170
  const onProcessSuccess = async (input) => {
170
171
  await TypedQueue.ack(input.queueName, input.message.id, input.driverName);
172
+ await recordQueueMonitorJob({
173
+ queueName: input.queueName,
174
+ status: 'completed',
175
+ job: {
176
+ id: input.message.id,
177
+ name: `${input.queueName}-job`,
178
+ data: input.message.payload,
179
+ attemptsMade: getAttemptsFromMessage(input.message),
180
+ processedOn: input.startedAtMs,
181
+ finishedOn: Date.now(),
182
+ },
183
+ });
171
184
  if (typeof input.trackerApi.completed === 'function') {
172
185
  await input.trackerApi.completed({
173
186
  queueName: input.queueName,
@@ -217,6 +230,20 @@ const onProcessFailure = async (input) => {
217
230
  });
218
231
  }
219
232
  await TypedQueue.ack(input.queueName, input.message.id, input.driverName);
233
+ await recordQueueMonitorJob({
234
+ queueName: input.queueName,
235
+ status: 'failed',
236
+ job: {
237
+ id: input.message.id,
238
+ name: `${input.queueName}-job`,
239
+ data: input.message.payload,
240
+ attemptsMade: nextAttempts,
241
+ failedReason: failure.message,
242
+ processedOn: Date.now(),
243
+ finishedOn: Date.now(),
244
+ },
245
+ error: failure,
246
+ });
220
247
  await removeHeartbeatIfSupported(input.queueName, input.message.id);
221
248
  if (typeof input.trackerApi.failed === 'function') {
222
249
  await input.trackerApi.failed({
@@ -0,0 +1,17 @@
1
+ type QueueMonitorStatus = 'completed' | 'failed';
2
+ type QueueMonitorJobLike = {
3
+ id?: string;
4
+ name?: string;
5
+ data?: unknown;
6
+ attemptsMade?: number;
7
+ failedReason?: string;
8
+ processedOn?: number;
9
+ finishedOn?: number;
10
+ };
11
+ export declare const recordQueueMonitorJob: (input: {
12
+ queueName: string;
13
+ status: QueueMonitorStatus;
14
+ job: QueueMonitorJobLike;
15
+ error?: Error;
16
+ }) => Promise<void>;
17
+ export {};
@@ -0,0 +1,62 @@
1
+ import { isNonEmptyString, isObject, Logger, queueConfig } from '@zintrust/core';
2
+ let queueMonitorMetricsPromise = null;
3
+ const toFiniteInteger = (value, fallback) => {
4
+ if (typeof value === 'number' && Number.isFinite(value)) {
5
+ return Math.floor(value);
6
+ }
7
+ if (typeof value === 'string' && value.trim() !== '') {
8
+ const parsed = Number(value);
9
+ if (Number.isFinite(parsed)) {
10
+ return Math.floor(parsed);
11
+ }
12
+ }
13
+ return fallback;
14
+ };
15
+ const resolveQueueMonitorRedisConfig = () => {
16
+ const redisConfig = queueConfig?.drivers?.redis;
17
+ if (!isObject(redisConfig) || redisConfig['driver'] !== 'redis') {
18
+ return null;
19
+ }
20
+ const host = isNonEmptyString(redisConfig['host']) ? redisConfig['host'].trim() : '127.0.0.1';
21
+ const port = toFiniteInteger(redisConfig['port'], 6379);
22
+ const db = toFiniteInteger(redisConfig['database'], 0);
23
+ const password = isNonEmptyString(redisConfig['password']) ? redisConfig['password'] : undefined;
24
+ return { host, port, password, db };
25
+ };
26
+ const loadQueueMonitorMetrics = async () => {
27
+ const redisConfig = resolveQueueMonitorRedisConfig();
28
+ if (redisConfig === null) {
29
+ return null;
30
+ }
31
+ try {
32
+ const module = (await import('@zintrust/queue-monitor'));
33
+ if (typeof module.createMetrics !== 'function') {
34
+ return null;
35
+ }
36
+ return module.createMetrics(redisConfig);
37
+ }
38
+ catch (error) {
39
+ Logger.debug('Queue monitor metrics are unavailable for worker history recording', error);
40
+ return null;
41
+ }
42
+ };
43
+ const getQueueMonitorMetrics = async () => {
44
+ queueMonitorMetricsPromise ??= loadQueueMonitorMetrics();
45
+ return queueMonitorMetricsPromise;
46
+ };
47
+ export const recordQueueMonitorJob = async (input) => {
48
+ const metrics = await getQueueMonitorMetrics();
49
+ if (metrics === null) {
50
+ return;
51
+ }
52
+ try {
53
+ await metrics.recordJob(input.queueName, input.status, input.job, input.error);
54
+ }
55
+ catch (error) {
56
+ Logger.debug('Queue monitor history write failed', {
57
+ queueName: input.queueName,
58
+ status: input.status,
59
+ error,
60
+ });
61
+ }
62
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zintrust/workers",
3
- "version": "0.4.50",
3
+ "version": "0.4.60",
4
4
  "description": "Worker orchestration and background job management for ZinTrust.",
5
5
  "private": false,
6
6
  "type": "module",
@@ -73,4 +73,4 @@
73
73
  "prom-client": "^15.1.3",
74
74
  "simple-statistics": "^7.8.9"
75
75
  }
76
- }
76
+ }
@@ -47,6 +47,7 @@ import { WorkerMetrics } from './WorkerMetrics';
47
47
  import { WorkerRegistry, type WorkerInstance as RegistryWorkerInstance } from './WorkerRegistry';
48
48
  import { WorkerVersioning } from './WorkerVersioning';
49
49
  import { keyPrefix } from './config/workerConfig';
50
+ import { recordQueueMonitorJob } from './queueMonitorHistory';
50
51
  import {
51
52
  DbWorkerStore,
52
53
  InMemoryWorkerStore,
@@ -2685,12 +2686,18 @@ const initializeDatacenter = (config: WorkerFactoryConfig): void => {
2685
2686
  const setupWorkerEventListeners = (
2686
2687
  worker: Worker,
2687
2688
  workerName: string,
2689
+ queueName: string,
2688
2690
  workerVersion: string,
2689
2691
  features?: WorkerFactoryConfig['features']
2690
2692
  ): void => {
2691
2693
  worker.on('completed', (job: Job) => {
2692
2694
  try {
2693
2695
  Logger.debug(`Job completed: ${workerName}`, { jobId: job.id });
2696
+ void recordQueueMonitorJob({
2697
+ queueName,
2698
+ status: 'completed',
2699
+ job,
2700
+ });
2694
2701
 
2695
2702
  if (features?.observability === true) {
2696
2703
  Observability.incrementCounter('worker.jobs.completed', 1, {
@@ -2707,6 +2714,14 @@ const setupWorkerEventListeners = (
2707
2714
  worker.on('failed', (job: Job | undefined, error: Error) => {
2708
2715
  try {
2709
2716
  Logger.error(`Job failed: ${workerName}`, { error, jobId: job?.id }, 'workers');
2717
+ if (job) {
2718
+ void recordQueueMonitorJob({
2719
+ queueName,
2720
+ status: 'failed',
2721
+ job,
2722
+ error,
2723
+ });
2724
+ }
2710
2725
 
2711
2726
  if (features?.observability === true) {
2712
2727
  Observability.incrementCounter('worker.jobs.failed', 1, {
@@ -2900,7 +2915,7 @@ export const WorkerFactory = Object.freeze({
2900
2915
  const resolvedOptions = resolveWorkerOptions(config, autoStart);
2901
2916
  const worker = new Worker(queueName, enhancedProcessor, resolvedOptions);
2902
2917
 
2903
- setupWorkerEventListeners(worker, name, workerVersion, features);
2918
+ setupWorkerEventListeners(worker, name, queueName, workerVersion, features);
2904
2919
 
2905
2920
  // Update status to "starting"
2906
2921
  await store.update(name, {
@@ -1,6 +1,7 @@
1
1
  import type { BullMQPayload, QueueMessage } from '@zintrust/core';
2
2
  import * as Core from '@zintrust/core';
3
3
  import { Env, Logger, Queue } from '@zintrust/core';
4
+ import { recordQueueMonitorJob } from './queueMonitorHistory';
4
5
 
5
6
  type QueueApi = Readonly<{
6
7
  enqueue: (queue: string, payload: BullMQPayload, driverName?: string) => Promise<string>;
@@ -320,6 +321,19 @@ const onProcessSuccess = async <TPayload>(input: {
320
321
  }): Promise<boolean> => {
321
322
  await TypedQueue.ack(input.queueName, input.message.id, input.driverName);
322
323
 
324
+ await recordQueueMonitorJob({
325
+ queueName: input.queueName,
326
+ status: 'completed',
327
+ job: {
328
+ id: input.message.id,
329
+ name: `${input.queueName}-job`,
330
+ data: input.message.payload,
331
+ attemptsMade: getAttemptsFromMessage(input.message),
332
+ processedOn: input.startedAtMs,
333
+ finishedOn: Date.now(),
334
+ },
335
+ });
336
+
323
337
  if (typeof input.trackerApi.completed === 'function') {
324
338
  await input.trackerApi.completed({
325
339
  queueName: input.queueName,
@@ -386,6 +400,22 @@ const onProcessFailure = async <TPayload>(input: {
386
400
  }
387
401
 
388
402
  await TypedQueue.ack(input.queueName, input.message.id, input.driverName);
403
+
404
+ await recordQueueMonitorJob({
405
+ queueName: input.queueName,
406
+ status: 'failed',
407
+ job: {
408
+ id: input.message.id,
409
+ name: `${input.queueName}-job`,
410
+ data: input.message.payload,
411
+ attemptsMade: nextAttempts,
412
+ failedReason: failure.message,
413
+ processedOn: Date.now(),
414
+ finishedOn: Date.now(),
415
+ },
416
+ error: failure,
417
+ });
418
+
389
419
  await removeHeartbeatIfSupported(input.queueName, input.message.id);
390
420
 
391
421
  if (typeof input.trackerApi.failed === 'function') {
@@ -0,0 +1,114 @@
1
+ import { isNonEmptyString, isObject, Logger, queueConfig } from '@zintrust/core';
2
+
3
+ type QueueMonitorStatus = 'completed' | 'failed';
4
+
5
+ type QueueMonitorJobLike = {
6
+ id?: string;
7
+ name?: string;
8
+ data?: unknown;
9
+ attemptsMade?: number;
10
+ failedReason?: string;
11
+ processedOn?: number;
12
+ finishedOn?: number;
13
+ };
14
+
15
+ type QueueMonitorMetrics = {
16
+ recordJob: (
17
+ queueName: string,
18
+ status: QueueMonitorStatus,
19
+ job: QueueMonitorJobLike,
20
+ error?: Error
21
+ ) => Promise<void>;
22
+ };
23
+
24
+ type QueueMonitorModule = {
25
+ createMetrics?: (config: {
26
+ host: string;
27
+ port: number;
28
+ password?: string;
29
+ db: number;
30
+ }) => QueueMonitorMetrics;
31
+ };
32
+
33
+ let queueMonitorMetricsPromise: Promise<QueueMonitorMetrics | null> | null = null;
34
+
35
+ const toFiniteInteger = (value: unknown, fallback: number): number => {
36
+ if (typeof value === 'number' && Number.isFinite(value)) {
37
+ return Math.floor(value);
38
+ }
39
+
40
+ if (typeof value === 'string' && value.trim() !== '') {
41
+ const parsed = Number(value);
42
+ if (Number.isFinite(parsed)) {
43
+ return Math.floor(parsed);
44
+ }
45
+ }
46
+
47
+ return fallback;
48
+ };
49
+
50
+ const resolveQueueMonitorRedisConfig = (): {
51
+ host: string;
52
+ port: number;
53
+ password?: string;
54
+ db: number;
55
+ } | null => {
56
+ const redisConfig = queueConfig?.drivers?.redis;
57
+
58
+ if (!isObject(redisConfig) || redisConfig['driver'] !== 'redis') {
59
+ return null;
60
+ }
61
+
62
+ const host = isNonEmptyString(redisConfig['host']) ? redisConfig['host'].trim() : '127.0.0.1';
63
+ const port = toFiniteInteger(redisConfig['port'], 6379);
64
+ const db = toFiniteInteger(redisConfig['database'], 0);
65
+ const password = isNonEmptyString(redisConfig['password']) ? redisConfig['password'] : undefined;
66
+
67
+ return { host, port, password, db };
68
+ };
69
+
70
+ const loadQueueMonitorMetrics = async (): Promise<QueueMonitorMetrics | null> => {
71
+ const redisConfig = resolveQueueMonitorRedisConfig();
72
+ if (redisConfig === null) {
73
+ return null;
74
+ }
75
+
76
+ try {
77
+ const module = (await import('@zintrust/queue-monitor')) as QueueMonitorModule;
78
+ if (typeof module.createMetrics !== 'function') {
79
+ return null;
80
+ }
81
+
82
+ return module.createMetrics(redisConfig);
83
+ } catch (error) {
84
+ Logger.debug('Queue monitor metrics are unavailable for worker history recording', error);
85
+ return null;
86
+ }
87
+ };
88
+
89
+ const getQueueMonitorMetrics = async (): Promise<QueueMonitorMetrics | null> => {
90
+ queueMonitorMetricsPromise ??= loadQueueMonitorMetrics();
91
+ return queueMonitorMetricsPromise;
92
+ };
93
+
94
+ export const recordQueueMonitorJob = async (input: {
95
+ queueName: string;
96
+ status: QueueMonitorStatus;
97
+ job: QueueMonitorJobLike;
98
+ error?: Error;
99
+ }): Promise<void> => {
100
+ const metrics = await getQueueMonitorMetrics();
101
+ if (metrics === null) {
102
+ return;
103
+ }
104
+
105
+ try {
106
+ await metrics.recordJob(input.queueName, input.status, input.job, input.error);
107
+ } catch (error) {
108
+ Logger.debug('Queue monitor history write failed', {
109
+ queueName: input.queueName,
110
+ status: input.status,
111
+ error,
112
+ });
113
+ }
114
+ };
@@ -1,4 +1,43 @@
1
1
  declare module '@zintrust/queue-monitor' {
2
+ export type JobStatus = 'completed' | 'failed';
3
+
4
+ export type JobSummary = {
5
+ id: string | undefined;
6
+ name: string;
7
+ queue?: string;
8
+ data: unknown;
9
+ attempts: number;
10
+ status?: string;
11
+ failedReason?: string;
12
+ timestamp: number;
13
+ processedOn?: number;
14
+ finishedOn?: number;
15
+ };
16
+
17
+ export type Metrics = {
18
+ recordJob: (
19
+ queue: string,
20
+ status: JobStatus,
21
+ job: {
22
+ id?: string;
23
+ name?: string;
24
+ data?: unknown;
25
+ attemptsMade?: number;
26
+ failedReason?: string;
27
+ processedOn?: number;
28
+ finishedOn?: number;
29
+ },
30
+ error?: Error
31
+ ) => Promise<void>;
32
+ getStats: (
33
+ queue: string,
34
+ minutes?: number
35
+ ) => Promise<Array<{ time: string; completed: number; failed: number }>>;
36
+ getRecentJobs: (queue: string) => Promise<JobSummary[]>;
37
+ getFailedJobs: (queue: string) => Promise<JobSummary[]>;
38
+ close: () => Promise<void>;
39
+ };
40
+
2
41
  export type QueueCounts = {
3
42
  waiting: number;
4
43
  active: number;
@@ -34,6 +73,13 @@ declare module '@zintrust/queue-monitor' {
34
73
  getSnapshot: () => Promise<QueueMonitorSnapshot>;
35
74
  };
36
75
 
76
+ export const createMetrics: (config: {
77
+ host: string;
78
+ port: number;
79
+ password?: string;
80
+ db: number;
81
+ }) => Metrics;
82
+
37
83
  export const QueueMonitor: Readonly<{
38
84
  create: (config: QueueMonitorConfig) => QueueMonitorApi;
39
85
  }>;