@zintrust/workers 0.4.27 → 0.4.36

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.
@@ -131,7 +131,7 @@ export const ClusterLock = Object.freeze({
131
131
  Logger.warn('ClusterLock already initialized');
132
132
  return;
133
133
  }
134
- const client = createRedisConnection(config);
134
+ const client = createRedisConnection(config, 3, { subsystem: 'worker-cluster-lock' });
135
135
  redisClient = client;
136
136
  startHeartbeat(client);
137
137
  Logger.info('ClusterLock initialized', { instanceId: getInstanceId() });
@@ -191,7 +191,7 @@ export const ComplianceManager = Object.freeze({
191
191
  Logger.warn('ComplianceManager already initialized');
192
192
  return;
193
193
  }
194
- redisClient = createRedisConnection(redisConfig);
194
+ redisClient = createRedisConnection(redisConfig, 3, { subsystem: 'worker-compliance' });
195
195
  complianceConfig = {
196
196
  gdpr: { ...DEFAULT_CONFIG.gdpr, ...config?.gdpr },
197
197
  hipaa: { ...DEFAULT_CONFIG.hipaa, ...config?.hipaa },
@@ -161,7 +161,7 @@ export const DeadLetterQueue = Object.freeze({
161
161
  Logger.warn('DeadLetterQueue already initialized');
162
162
  return;
163
163
  }
164
- redisClient = createRedisConnection(config);
164
+ redisClient = createRedisConnection(config, 3, { subsystem: 'worker-dlq' });
165
165
  retentionPolicy = policy;
166
166
  // Start cleanup interval if auto-delete is enabled
167
167
  if (policy.enabled && policy.autoDeleteAfterDays !== undefined) {
@@ -100,7 +100,6 @@ const resolvePackageSpecifierUrl = (specifier) => {
100
100
  return resolveLocalPackageFallback(specifier);
101
101
  }
102
102
  };
103
- const escapeRegExp = (value) => value.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw `\$&`);
104
103
  const rewriteProcessorImports = (code) => {
105
104
  const replacements = [];
106
105
  const coreUrl = resolvePackageSpecifierUrl('@zintrust/core');
@@ -113,8 +112,8 @@ const rewriteProcessorImports = (code) => {
113
112
  return code;
114
113
  let updated = code;
115
114
  for (const { from, to } of replacements) {
116
- const pattern = new RegExp(String.raw `(['"])${escapeRegExp(from)}\1`, 'g');
117
- updated = updated.replace(pattern, `$1${to}$1`);
115
+ updated = updated.replaceAll(`'${from}'`, `'${to}'`);
116
+ updated = updated.replaceAll(`"${from}"`, `"${to}"`);
118
117
  }
119
118
  return updated;
120
119
  };
@@ -330,14 +329,43 @@ const parseCacheControl = (value) => {
330
329
  };
331
330
  const getProcessorSpecConfig = () => workersConfig.processorSpec;
332
331
  const toPosixPath = (value) => value.split(path.sep).join('/');
332
+ const isUpperAlpha = (value) => /^[A-Z]$/.test(value);
333
+ const isLowerAlphaOrDigit = (value) => /^[a-z\d]$/.test(value);
334
+ const isAlphaNumeric = (value) => /^[A-Za-z\d]$/.test(value);
335
+ const shouldInsertWorkerNameDash = (previous, current, next) => {
336
+ if (!isAlphaNumeric(previous) || !isAlphaNumeric(current))
337
+ return false;
338
+ if (isLowerAlphaOrDigit(previous) && isUpperAlpha(current)) {
339
+ return true;
340
+ }
341
+ if (isUpperAlpha(previous) && isUpperAlpha(current) && isLowerAlphaOrDigit(next ?? '')) {
342
+ return true;
343
+ }
344
+ return false;
345
+ };
346
+ const toKebabWorkerName = (value) => {
347
+ if (!isNonEmptyString(value))
348
+ return value;
349
+ let normalized = '';
350
+ for (let index = 0; index < value.length; index += 1) {
351
+ const current = value[index] ?? '';
352
+ const previous = index > 0 ? (value[index - 1] ?? '') : '';
353
+ const next = value[index + 1];
354
+ if (current === ' ' || current === '_') {
355
+ if (!normalized.endsWith('-'))
356
+ normalized += '-';
357
+ continue;
358
+ }
359
+ if (shouldInsertWorkerNameDash(previous, current, next) && !normalized.endsWith('-')) {
360
+ normalized += '-';
361
+ }
362
+ normalized += current;
363
+ }
364
+ return normalized.replaceAll(/-+/g, '-');
365
+ };
333
366
  const normalizeWorkerFileName = (fileName) => {
334
- const baseName = fileName.replaceAll(/\.[^.]+$/, '');
335
- return baseName
336
- .replaceAll(/([a-z\d])([A-Z])/g, '$1-$2')
337
- .replaceAll(/([A-Z]+)([A-Z][a-z\d]+)/g, '$1-$2')
338
- .replaceAll(/[\s_]+/g, '-')
339
- .replaceAll(/-+/g, '-')
340
- .toLowerCase();
367
+ const baseName = fileName.replace(/\.[^.]+$/, '');
368
+ return toKebabWorkerName(baseName).toLowerCase();
341
369
  };
342
370
  const supportsWorkerFileDiscovery = () => {
343
371
  return (isNodeRuntime() &&
@@ -1364,19 +1392,15 @@ const resolveRedisConfigFromEnv = (config, context) => {
1364
1392
  };
1365
1393
  const resolveRedisConfigFromDirect = (config, context) => {
1366
1394
  const fallbackDb = Env.getInt('REDIS_QUEUE_DB', ZintrustLang.REDIS_DEFAULT_DB);
1367
- const redisConfigWithDatabase = config;
1368
1395
  let normalizedDb = fallbackDb;
1369
1396
  if (typeof config.db === 'number') {
1370
1397
  normalizedDb = config.db;
1371
1398
  }
1372
- else if (typeof redisConfigWithDatabase.database === 'number') {
1373
- normalizedDb = redisConfigWithDatabase.database;
1374
- }
1375
1399
  return {
1376
1400
  host: requireRedisHost(config.host, context),
1377
1401
  port: config.port,
1378
1402
  db: normalizedDb,
1379
- password: config.password ?? Env.get('REDIS_PASSWORD', undefined),
1403
+ password: config.password ?? Env.get('REDIS_PASSWORD'),
1380
1404
  };
1381
1405
  };
1382
1406
  const resolveRedisConfig = (config, context) => isRedisEnvConfig(config)
@@ -1498,7 +1522,7 @@ const resolveWorkerStore = async (config) => {
1498
1522
  const redisConfig = resolveRedisConfigWithFallback(persistence.redis, config.infrastructure?.redis, 'Worker persistence requires redis config (persistence.redis or infrastructure.redis)', 'infrastructure.persistence.redis');
1499
1523
  const key_prefix = persistence.keyPrefix ?? keyPrefix();
1500
1524
  logRedisPersistenceConfig(redisConfig, key_prefix, 'resolveWorkerStore');
1501
- const client = createRedisConnection(redisConfig);
1525
+ const client = createRedisConnection(redisConfig, 3, { subsystem: 'worker-persistence' });
1502
1526
  next = RedisWorkerStore.create(client, key_prefix);
1503
1527
  }
1504
1528
  else if (persistence.driver === 'database') {
@@ -1542,7 +1566,7 @@ const createWorkerStore = async (persistence) => {
1542
1566
  const redisConfig = resolveRedisConfigWithFallback(persistence.redis ?? { env: true }, undefined, 'Worker persistence requires redis config (persistence.redis or REDIS_* env values)', 'persistence.redis');
1543
1567
  const key_prefix = persistence.keyPrefix ?? keyPrefix();
1544
1568
  logRedisPersistenceConfig(redisConfig, key_prefix, 'createWorkerStore');
1545
- const client = createRedisConnection(redisConfig);
1569
+ const client = createRedisConnection(redisConfig, 3, { subsystem: 'worker-persistence' });
1546
1570
  return RedisWorkerStore.create(client, key_prefix);
1547
1571
  }
1548
1572
  // Database driver
@@ -31,7 +31,7 @@ const getValidClient = async () => {
31
31
  }
32
32
  // If no client, create one
33
33
  if (!redisClient) {
34
- redisClient = createRedisConnection(cachedConfig);
34
+ redisClient = createRedisConnection(cachedConfig, 3, { subsystem: 'worker-metrics' });
35
35
  }
36
36
  const client = redisClient;
37
37
  if (!client) {
@@ -220,7 +220,7 @@ const WorkerMetrics = Object.freeze({
220
220
  return;
221
221
  }
222
222
  cachedConfig = config;
223
- redisClient = createRedisConnection(config);
223
+ redisClient = createRedisConnection(config, 3, { subsystem: 'worker-metrics' });
224
224
  Logger.info('WorkerMetrics initialized');
225
225
  },
226
226
  /**
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "@zintrust/workers",
3
- "version": "0.1.52",
4
- "buildDate": "2026-03-26T09:12:29.177Z",
3
+ "version": "0.4.36",
4
+ "buildDate": "2026-03-30T17:20:12.328Z",
5
5
  "buildEnvironment": {
6
- "node": "v25.6.1",
6
+ "node": "v22.22.1",
7
7
  "platform": "darwin",
8
8
  "arch": "arm64"
9
9
  },
10
10
  "git": {
11
- "commit": "597f453f",
12
- "branch": "dev"
11
+ "commit": "a45b4628",
12
+ "branch": "release"
13
13
  },
14
14
  "package": {
15
15
  "engines": {
@@ -19,7 +19,6 @@
19
19
  "@opentelemetry/api",
20
20
  "hot-shots",
21
21
  "ioredis",
22
- "ml.js",
23
22
  "prom-client",
24
23
  "simple-statistics"
25
24
  ],
@@ -47,8 +46,8 @@
47
46
  "sha256": "1a6d9ea39ba9e17fc16eb52294c10c3912024cb3009a8f9c07ffa576e754c7f4"
48
47
  },
49
48
  "BroadcastWorker.d.ts": {
50
- "size": 793,
51
- "sha256": "63d5877e1363fd8f43cd0b0f63efca27a46e46fc2203cc073bd4110bb8e673b9"
49
+ "size": 934,
50
+ "sha256": "ea5b05820fdd30d80f4483f476e400c78fa842ebbb29a442b2f1261dc0b2de8a"
52
51
  },
53
52
  "BroadcastWorker.js": {
54
53
  "size": 821,
@@ -83,16 +82,16 @@
83
82
  "sha256": "77439b1e80a344ff2de380082886addda0e8ca0419e57e7039e9f6ad4c596462"
84
83
  },
85
84
  "ClusterLock.js": {
86
- "size": 13447,
87
- "sha256": "bd7da9158c7badf096057cf44d45d244a4a2fd5e2e34c7a858642ccf943645f2"
85
+ "size": 13488,
86
+ "sha256": "d216923cb5829a14155f2acfd7691075e9ee1d262fd25711afe877a7920642ca"
88
87
  },
89
88
  "ComplianceManager.d.ts": {
90
89
  "size": 5142,
91
90
  "sha256": "19a2695093bc102680cd8f28775d2b2b5efea5ad52b682c89fedbf4424b4b711"
92
91
  },
93
92
  "ComplianceManager.js": {
94
- "size": 19614,
95
- "sha256": "e29a36bbf663f30da08a59200b0fbb760e67f908faa95ed56e21aac2868f1b0f"
93
+ "size": 19653,
94
+ "sha256": "79fd228c4bae3d13fa64a75809fb112c65624560558fa57d6b7544a5ad1cf4f5"
96
95
  },
97
96
  "DatacenterOrchestrator.d.ts": {
98
97
  "size": 3532,
@@ -107,8 +106,8 @@
107
106
  "sha256": "dd82e7e2c23a2fcaefc0cb87a1a7931594d6a5f76b758f608c77063f5e8533cd"
108
107
  },
109
108
  "DeadLetterQueue.js": {
110
- "size": 19444,
111
- "sha256": "d17c60d3b938ed141760c7cc8b75b14e8c20ce98c3962265f4d12f2f1edba69c"
109
+ "size": 19476,
110
+ "sha256": "de0ac7ee53a71e375536041a3ce65be22bc29f1901af8564f1e60307007c061e"
112
111
  },
113
112
  "HealthMonitor.d.ts": {
114
113
  "size": 1485,
@@ -127,8 +126,8 @@
127
126
  "sha256": "23837171b31c5242e4b43226cb0acae815dea0ce7b1628aa1428a86b24689230"
128
127
  },
129
128
  "NotificationWorker.d.ts": {
130
- "size": 811,
131
- "sha256": "dd22674e6955c32c7ac7af8af8fee583e62d4f0e224da14a72a7f1799e734f96"
129
+ "size": 952,
130
+ "sha256": "facdae5d7e82364ad9aab4398f8a3f8b5be8d8e9cb3aaa18fde76a88cd05b959"
132
131
  },
133
132
  "NotificationWorker.js": {
134
133
  "size": 828,
@@ -175,28 +174,28 @@
175
174
  "sha256": "555277c91f87899415cebf35eddc08d7c0b80cc55bfac7cb7fe96529cc927257"
176
175
  },
177
176
  "WorkerFactory.d.ts": {
178
- "size": 7447,
179
- "sha256": "b31131698eae955a7fcd6d3ef872d0bc1d3557c7f18695850e9407707fcf548d"
177
+ "size": 7716,
178
+ "sha256": "3869f960c87260588e40941ff91bffcfa0757be7a04815fd28b57dd4840c51df"
180
179
  },
181
180
  "WorkerFactory.js": {
182
- "size": 90728,
183
- "sha256": "231c5af0f6b472649e82a9a062479f68dec18ea8c8298443e40d02fe3c9338f4"
181
+ "size": 102863,
182
+ "sha256": "1024756e603ca67461a955723dc004de9395267e50c7f59c9f8f0c13b2f0f7d8"
184
183
  },
185
184
  "WorkerInit.d.ts": {
186
- "size": 2391,
187
- "sha256": "8a6fdec7b592dd92162b3ad1b3d3fd6462abcf11028e510dbdb747748774d626"
185
+ "size": 3284,
186
+ "sha256": "f902550cda7fca36f2db6297d39a4339ed6b5ff1c738da867233309cf0ca8fe1"
188
187
  },
189
188
  "WorkerInit.js": {
190
- "size": 11908,
191
- "sha256": "71e4fd64b44d48b0bbd2fbebfb71fcf75c528734e90359994c492f9cc2590df6"
189
+ "size": 14031,
190
+ "sha256": "51f44ca085a1682130aa36fed3acfd575108e974bf4c21eaa772001719c18853"
192
191
  },
193
192
  "WorkerMetrics.d.ts": {
194
193
  "size": 3304,
195
194
  "sha256": "5b684f87668bba86825625c20dda162915674e8c0fe3c3a32e05a2d9951c985f"
196
195
  },
197
196
  "WorkerMetrics.js": {
198
- "size": 20584,
199
- "sha256": "51dfef790aa2531387b193c5194ef3fbe021a238e0b021666aab4a605ced5879"
197
+ "size": 20656,
198
+ "sha256": "a3888a82cdc8311887e1320a320d11630cf9358d80681be21f6fc02c90fee2fa"
200
199
  },
201
200
  "WorkerRegistry.d.ts": {
202
201
  "size": 3941,
@@ -231,8 +230,8 @@
231
230
  "sha256": "8af20d462270e7044c6ea983821f5b6e6ce8a5caf39b6e8fefff07c9a0bf071e"
232
231
  },
233
232
  "build-manifest.json": {
234
- "size": 19603,
235
- "sha256": "ee6386bb995df82724f76c33f1333c0f77863bff9c3cb6007ac2fd20969dec90"
233
+ "size": 19591,
234
+ "sha256": "09f2e951c3a969cd97fcfc9ed8133e14da9e4b4250453c84d69ef973dd7bedec"
236
235
  },
237
236
  "config/workerConfig.d.ts": {
238
237
  "size": 132,
@@ -243,12 +242,12 @@
243
242
  "sha256": "189c3b1d8a31de1c04206fcfacc1fed974a74684d7d70895cd20a03b35860ac6"
244
243
  },
245
244
  "createQueueWorker.d.ts": {
246
- "size": 890,
247
- "sha256": "c0e07ac27f1ec74fff817ce82eb1cbcfae45ab3cd079748766dcbf4a73c5ec98"
245
+ "size": 1031,
246
+ "sha256": "dacd49f6c112eba439bdd9bb457eea90daedbf32efc381cd3189ce562fa5b0a8"
248
247
  },
249
248
  "createQueueWorker.js": {
250
- "size": 13717,
251
- "sha256": "b8cfe312445a150824d26ba3867147aa6a10e8dece2e342c7447404fb7fa4991"
249
+ "size": 14103,
250
+ "sha256": "8e619da00200c0c1270674a6a2941ae050c8ade3e38925b5446916cddc2a8b65"
252
251
  },
253
252
  "dashboard/index.d.ts": {
254
253
  "size": 109,
@@ -271,8 +270,8 @@
271
270
  "sha256": "8e0e04329e1119d8ae835dd4458efead084293bcc2c263c09dd5a19d467e5ca4"
272
271
  },
273
272
  "dashboard/workers-api.js": {
274
- "size": 26774,
275
- "sha256": "8d49b06c88df07afabfb49a4d3a4459e3f40340eccd03610d6be9095409e422e"
273
+ "size": 28207,
274
+ "sha256": "ae1ff0e962b1f64e6d200a35c3b8b1de968341d8df6283625a3efce50a23826c"
276
275
  },
277
276
  "helper/index.d.ts": {
278
277
  "size": 159,
@@ -411,20 +410,20 @@
411
410
  "sha256": "e90a171f2d8f6a62518099f825775b80299b934a7d48421bc1a3ebd6b065d617"
412
411
  },
413
412
  "index.d.ts": {
414
- "size": 2601,
415
- "sha256": "6943403aba7442451a5abd3eb64a12d2842bb310360477b5a7180cf3b4373efa"
413
+ "size": 2721,
414
+ "sha256": "0dcb7ca21683ab210eeb23d067e41b55ae996fa9e52c688371dc177f070ec059"
416
415
  },
417
416
  "index.js": {
418
- "size": 2261,
419
- "sha256": "afd4bf4715321d1e073366a7cb93ac17a1f9f647500ac4027102de65f26bc213"
417
+ "size": 2337,
418
+ "sha256": "d6f01d1d1f313d649fd5072e70617e3e28061e9c55d25ce24c3ea2d06e2cb8eb"
420
419
  },
421
420
  "register.d.ts": {
422
421
  "size": 256,
423
422
  "sha256": "07753654e043fd8ac0d42c4d74a26406929fc076c8c86b3d0513898c7da0d0aa"
424
423
  },
425
424
  "register.js": {
426
- "size": 1555,
427
- "sha256": "993d453ba69ea637d3674684032fc6dd47755c23c491920c04d12e006290cfe6"
425
+ "size": 1626,
426
+ "sha256": "4c404dfb2df0ac42da904bf30cece14ddbe127b8408d1ded951bf60ca86d07a3"
428
427
  },
429
428
  "routes/workers.d.ts": {
430
429
  "size": 498,
@@ -1,5 +1,6 @@
1
1
  import * as Core from '@zintrust/core';
2
2
  import { Env, Logger, Queue } from '@zintrust/core';
3
+ const TypedQueue = Queue;
3
4
  const RETRY_BASE_DELAY_MS = 1000;
4
5
  const RETRY_MAX_DELAY_MS = 30000;
5
6
  const getJobStateTracker = () => {
@@ -92,6 +93,12 @@ const buildBaseLogFields = (message, getLogFields) => {
92
93
  ...getLogFields(message.payload),
93
94
  };
94
95
  };
96
+ const toBullMQPayload = (payload) => {
97
+ if (typeof payload === 'object' && payload !== null) {
98
+ return { ...payload };
99
+ }
100
+ return { payload };
101
+ };
95
102
  const getWorkerInstanceId = () => {
96
103
  return typeof Env['WORKER_INSTANCE_ID'] === 'string'
97
104
  ? String(Env['WORKER_INSTANCE_ID'])
@@ -139,12 +146,12 @@ const checkAndRequeueIfNotDue = async (options, queueName, driverName, message,
139
146
  ...baseLogFields,
140
147
  dueAt: new Date(timestamp).toISOString(),
141
148
  });
142
- await Queue.enqueue(queueName, message.payload, driverName);
143
- await Queue.ack(queueName, message.id, driverName);
149
+ await TypedQueue.enqueue(queueName, toBullMQPayload(message.payload), driverName);
150
+ await TypedQueue.ack(queueName, message.id, driverName);
144
151
  return true;
145
152
  };
146
153
  const onProcessSuccess = async (input) => {
147
- await Queue.ack(input.queueName, input.message.id, input.driverName);
154
+ await TypedQueue.ack(input.queueName, input.message.id, input.driverName);
148
155
  if (typeof input.trackerApi.completed === 'function') {
149
156
  await input.trackerApi.completed({
150
157
  queueName: input.queueName,
@@ -177,22 +184,20 @@ const onProcessFailure = async (input) => {
177
184
  if (nextAttempts < input.options.maxAttempts) {
178
185
  const retryDelayMs = getRetryDelayMs(nextAttempts);
179
186
  retryAt = new Date(Date.now() + retryDelayMs).toISOString();
180
- const currentPayload = typeof input.message.payload === 'object' && input.message.payload !== null
181
- ? input.message.payload
182
- : { payload: input.message.payload };
187
+ const currentPayload = toBullMQPayload(input.message.payload);
183
188
  const payloadForRetry = {
184
189
  ...currentPayload,
185
190
  attempts: nextAttempts,
186
191
  timestamp: Date.now() + retryDelayMs,
187
192
  };
188
- await Queue.enqueue(input.queueName, payloadForRetry, input.driverName);
193
+ await TypedQueue.enqueue(input.queueName, payloadForRetry, input.driverName);
189
194
  Logger.info(`${input.options.kindLabel} re-queued for retry`, {
190
195
  ...input.baseLogFields,
191
196
  attempts: nextAttempts,
192
197
  retryDelayMs,
193
198
  });
194
199
  }
195
- await Queue.ack(input.queueName, input.message.id, input.driverName);
200
+ await TypedQueue.ack(input.queueName, input.message.id, input.driverName);
196
201
  await removeHeartbeatIfSupported(input.queueName, input.message.id);
197
202
  if (typeof input.trackerApi.failed === 'function') {
198
203
  await input.trackerApi.failed({
@@ -235,7 +240,7 @@ const startTrackingAndHeartbeat = async (input) => {
235
240
  return { startedAtMs, heartbeatTimer };
236
241
  };
237
242
  const processQueueMessage = async (options, queueName, driverName) => {
238
- const message = (await Queue.dequeue(queueName, driverName));
243
+ const message = await TypedQueue.dequeue(queueName, driverName);
239
244
  if (!message)
240
245
  return false;
241
246
  const baseLogFields = buildBaseLogFields(message, options.getLogFields);
package/dist/index.d.ts CHANGED
@@ -32,11 +32,12 @@ export { BroadcastWorker } from './BroadcastWorker';
32
32
  export { createQueueWorker } from './createQueueWorker';
33
33
  export type { CreateQueueWorkerOptions } from './createQueueWorker';
34
34
  export { NotificationWorker } from './NotificationWorker';
35
- export type { RedisConfig, WorkerAutoScalingConfig, WorkerComplianceConfig, WorkerConfig, WorkerCostConfig, WorkerObservabilityConfig, WorkerStatus, WorkerVersioningConfig, WorkersConfigOverrides, WorkersGlobalConfig, } from '@zintrust/core';
35
+ export type { RedisConfig, WorkerAutoScalingConfig, WorkerComplianceConfig, WorkerConfig, WorkerCostConfig, WorkerObservabilityConfig, WorkersConfigOverrides, WorkersGlobalConfig, WorkerStatus, WorkerVersioningConfig, } from '@zintrust/core';
36
36
  export type { Job, Worker, WorkerOptions } from 'bullmq';
37
37
  export type { IAnomaly, IAnomalyConfig, IForecast, IMetric, IPrediction, IRecommendation, IRootCauseAnalysis, } from './AnomalyDetection';
38
38
  export type { IChaosComparison, IChaosExperiment, IChaosReport, IChaosStatus, } from './ChaosEngineering';
39
39
  export type { ISLAConfig, ISLAReport, ISLAStatus, ISLAViolation, ITimeRange } from './SLAMonitor';
40
+ export type * from './config/workerConfig';
40
41
  export type * from './type';
41
42
  /**
42
43
  * Package version and build metadata
package/dist/register.js CHANGED
@@ -16,6 +16,7 @@ const getWorkerProviders = () => {
16
16
  ['worker:start-all', WorkerCommands.createWorkerStartAllCommand()],
17
17
  ['worker:stop', WorkerCommands.createWorkerStopCommand()],
18
18
  ['worker:restart', WorkerCommands.createWorkerRestartCommand()],
19
+ ['worker:doctor', WorkerCommands.createWorkerDoctorCommand()],
19
20
  ['worker:summary', WorkerCommands.createWorkerSummaryCommand()],
20
21
  ];
21
22
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zintrust/workers",
3
- "version": "0.4.27",
3
+ "version": "0.4.36",
4
4
  "description": "Worker orchestration and background job management for ZinTrust.",
5
5
  "private": false,
6
6
  "type": "module",
@@ -40,7 +40,7 @@
40
40
  "node": ">=20.0.0"
41
41
  },
42
42
  "peerDependencies": {
43
- "@zintrust/core": "^0.4.27",
43
+ "@zintrust/core": "^0.4.36",
44
44
  "@zintrust/queue-monitor": "*",
45
45
  "@zintrust/queue-redis": "*"
46
46
  },
@@ -67,10 +67,10 @@
67
67
  "prepublishOnly": "npm run build"
68
68
  },
69
69
  "dependencies": {
70
- "@opentelemetry/api": "^1.9.0",
70
+ "@opentelemetry/api": "^1.9.1",
71
71
  "hot-shots": "^14.2.0",
72
- "ioredis": "^5.10.0",
72
+ "ioredis": "^5.10.1",
73
73
  "prom-client": "^15.1.3",
74
74
  "simple-statistics": "^7.8.9"
75
75
  }
76
- }
76
+ }
@@ -193,7 +193,7 @@ export const ClusterLock = Object.freeze({
193
193
  return;
194
194
  }
195
195
 
196
- const client = createRedisConnection(config);
196
+ const client = createRedisConnection(config, 3, { subsystem: 'worker-cluster-lock' });
197
197
  redisClient = client;
198
198
  startHeartbeat(client);
199
199
 
@@ -344,7 +344,7 @@ export const ComplianceManager = Object.freeze({
344
344
  return;
345
345
  }
346
346
 
347
- redisClient = createRedisConnection(redisConfig);
347
+ redisClient = createRedisConnection(redisConfig, 3, { subsystem: 'worker-compliance' });
348
348
  complianceConfig = {
349
349
  gdpr: { ...DEFAULT_CONFIG.gdpr, ...config?.gdpr },
350
350
  hipaa: { ...DEFAULT_CONFIG.hipaa, ...config?.hipaa },
@@ -260,7 +260,7 @@ export const DeadLetterQueue = Object.freeze({
260
260
  return;
261
261
  }
262
262
 
263
- redisClient = createRedisConnection(config);
263
+ redisClient = createRedisConnection(config, 3, { subsystem: 'worker-dlq' });
264
264
  retentionPolicy = policy;
265
265
 
266
266
  // Start cleanup interval if auto-delete is enabled
@@ -141,9 +141,6 @@ const resolvePackageSpecifierUrl = (specifier: string): string | null => {
141
141
  }
142
142
  };
143
143
 
144
- const escapeRegExp = (value: string): string =>
145
- value.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
146
-
147
144
  const rewriteProcessorImports = (code: string): string => {
148
145
  const replacements: Array<{ from: string; to: string }> = [];
149
146
  const coreUrl = resolvePackageSpecifierUrl('@zintrust/core');
@@ -155,8 +152,8 @@ const rewriteProcessorImports = (code: string): string => {
155
152
 
156
153
  let updated = code;
157
154
  for (const { from, to } of replacements) {
158
- const pattern = new RegExp(String.raw`(['"])${escapeRegExp(from)}\1`, 'g');
159
- updated = updated.replace(pattern, `$1${to}$1`);
155
+ updated = updated.replaceAll(`'${from}'`, `'${to}'`);
156
+ updated = updated.replaceAll(`"${from}"`, `"${to}"`);
160
157
  }
161
158
 
162
159
  return updated;
@@ -548,14 +545,58 @@ const getProcessorSpecConfig = (): typeof workersConfig.processorSpec =>
548
545
 
549
546
  const toPosixPath = (value: string): string => value.split(path.sep).join('/');
550
547
 
548
+ const isUpperAlpha = (value: string): boolean => /^[A-Z]$/.test(value);
549
+
550
+ const isLowerAlphaOrDigit = (value: string): boolean => /^[a-z\d]$/.test(value);
551
+
552
+ const isAlphaNumeric = (value: string): boolean => /^[A-Za-z\d]$/.test(value);
553
+
554
+ const shouldInsertWorkerNameDash = (
555
+ previous: string,
556
+ current: string,
557
+ next: string | undefined
558
+ ): boolean => {
559
+ if (!isAlphaNumeric(previous) || !isAlphaNumeric(current)) return false;
560
+
561
+ if (isLowerAlphaOrDigit(previous) && isUpperAlpha(current)) {
562
+ return true;
563
+ }
564
+
565
+ if (isUpperAlpha(previous) && isUpperAlpha(current) && isLowerAlphaOrDigit(next ?? '')) {
566
+ return true;
567
+ }
568
+
569
+ return false;
570
+ };
571
+
572
+ const toKebabWorkerName = (value: string): string => {
573
+ if (!isNonEmptyString(value)) return value;
574
+
575
+ let normalized = '';
576
+
577
+ for (let index = 0; index < value.length; index += 1) {
578
+ const current = value[index] ?? '';
579
+ const previous = index > 0 ? (value[index - 1] ?? '') : '';
580
+ const next = value[index + 1];
581
+
582
+ if (current === ' ' || current === '_') {
583
+ if (!normalized.endsWith('-')) normalized += '-';
584
+ continue;
585
+ }
586
+
587
+ if (shouldInsertWorkerNameDash(previous, current, next) && !normalized.endsWith('-')) {
588
+ normalized += '-';
589
+ }
590
+
591
+ normalized += current;
592
+ }
593
+
594
+ return normalized.replaceAll(/-+/g, '-');
595
+ };
596
+
551
597
  const normalizeWorkerFileName = (fileName: string): string => {
552
- const baseName = fileName.replaceAll(/\.[^.]+$/, '');
553
- return baseName
554
- .replaceAll(/([a-z\d])([A-Z])/g, '$1-$2')
555
- .replaceAll(/([A-Z]+)([A-Z][a-z\d]+)/g, '$1-$2')
556
- .replaceAll(/[\s_]+/g, '-')
557
- .replaceAll(/-+/g, '-')
558
- .toLowerCase();
598
+ const baseName = fileName.replace(/\.[^.]+$/, '');
599
+ return toKebabWorkerName(baseName).toLowerCase();
559
600
  };
560
601
 
561
602
  const supportsWorkerFileDiscovery = (): boolean => {
@@ -1984,20 +2025,17 @@ const resolveRedisConfigFromEnv = (config: RedisEnvConfig, context: string): Red
1984
2025
 
1985
2026
  const resolveRedisConfigFromDirect = (config: RedisConfig, context: string): RedisConfig => {
1986
2027
  const fallbackDb = Env.getInt('REDIS_QUEUE_DB', ZintrustLang.REDIS_DEFAULT_DB);
1987
- const redisConfigWithDatabase = config as RedisConfig & { database?: number };
1988
2028
 
1989
2029
  let normalizedDb = fallbackDb;
1990
2030
  if (typeof config.db === 'number') {
1991
2031
  normalizedDb = config.db;
1992
- } else if (typeof redisConfigWithDatabase.database === 'number') {
1993
- normalizedDb = redisConfigWithDatabase.database;
1994
2032
  }
1995
2033
 
1996
2034
  return {
1997
2035
  host: requireRedisHost(config.host, context),
1998
2036
  port: config.port,
1999
2037
  db: normalizedDb,
2000
- password: config.password ?? Env.get('REDIS_PASSWORD', undefined),
2038
+ password: config.password ?? Env.get('REDIS_PASSWORD'),
2001
2039
  };
2002
2040
  };
2003
2041
 
@@ -2163,7 +2201,7 @@ const resolveWorkerStore = async (config: WorkerFactoryConfig): Promise<WorkerSt
2163
2201
  );
2164
2202
  const key_prefix = persistence.keyPrefix ?? keyPrefix();
2165
2203
  logRedisPersistenceConfig(redisConfig, key_prefix, 'resolveWorkerStore');
2166
- const client = createRedisConnection(redisConfig);
2204
+ const client = createRedisConnection(redisConfig, 3, { subsystem: 'worker-persistence' });
2167
2205
  next = RedisWorkerStore.create(client, key_prefix);
2168
2206
  } else if (persistence.driver === 'database') {
2169
2207
  const explicitConnection =
@@ -2217,7 +2255,7 @@ const createWorkerStore = async (persistence: WorkerPersistenceConfig): Promise<
2217
2255
  );
2218
2256
  const key_prefix = persistence.keyPrefix ?? keyPrefix();
2219
2257
  logRedisPersistenceConfig(redisConfig, key_prefix, 'createWorkerStore');
2220
- const client = createRedisConnection(redisConfig);
2258
+ const client = createRedisConnection(redisConfig, 3, { subsystem: 'worker-persistence' });
2221
2259
  return RedisWorkerStore.create(client, key_prefix);
2222
2260
  }
2223
2261
 
@@ -111,7 +111,7 @@ const getValidClient = async (): Promise<RedisConnection> => {
111
111
 
112
112
  // If no client, create one
113
113
  if (!redisClient) {
114
- redisClient = createRedisConnection(cachedConfig);
114
+ redisClient = createRedisConnection(cachedConfig, 3, { subsystem: 'worker-metrics' });
115
115
  }
116
116
 
117
117
  const client = redisClient;
@@ -358,7 +358,7 @@ const WorkerMetrics = Object.freeze({
358
358
  }
359
359
 
360
360
  cachedConfig = config;
361
- redisClient = createRedisConnection(config);
361
+ redisClient = createRedisConnection(config, 3, { subsystem: 'worker-metrics' });
362
362
  Logger.info('WorkerMetrics initialized');
363
363
  },
364
364
 
@@ -2,6 +2,17 @@ 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
4
 
5
+ type QueueApi = Readonly<{
6
+ enqueue: (queue: string, payload: BullMQPayload, driverName?: string) => Promise<string>;
7
+ dequeue: <TPayload>(
8
+ queue: string,
9
+ driverName?: string
10
+ ) => Promise<QueueMessage<TPayload> | undefined>;
11
+ ack: (queue: string, id: string, driverName?: string) => Promise<void>;
12
+ }>;
13
+
14
+ const TypedQueue = Queue as QueueApi;
15
+
5
16
  const RETRY_BASE_DELAY_MS = 1000;
6
17
  const RETRY_MAX_DELAY_MS = 30000;
7
18
 
@@ -97,9 +108,7 @@ const getAttemptsFromMessage = <TPayload>(message: QueueMessage<TPayload>): numb
97
108
  typeof message.payload === 'object' && message.payload !== null
98
109
  ? normalizeAttempts((message.payload as Record<string, unknown>)['attempts'])
99
110
  : 0;
100
- const messageAttempts = normalizeAttempts(
101
- (message as QueueMessage<TPayload> & { attempts?: number }).attempts
102
- );
111
+ const messageAttempts = normalizeAttempts(message.attempts);
103
112
  return Math.max(payloadAttempts, messageAttempts);
104
113
  };
105
114
 
@@ -152,6 +161,14 @@ const buildBaseLogFields = <TPayload>(
152
161
  };
153
162
  };
154
163
 
164
+ const toBullMQPayload = <TPayload>(payload: TPayload): BullMQPayload => {
165
+ if (typeof payload === 'object' && payload !== null) {
166
+ return { ...(payload as Record<string, unknown>) };
167
+ }
168
+
169
+ return { payload };
170
+ };
171
+
155
172
  type TrackerApi = {
156
173
  started?: (input: {
157
174
  queueName: string;
@@ -263,8 +280,8 @@ const checkAndRequeueIfNotDue = async <TPayload>(
263
280
  ...baseLogFields,
264
281
  dueAt: new Date(timestamp).toISOString(),
265
282
  });
266
- await Queue.enqueue(queueName, message.payload as BullMQPayload, driverName);
267
- await Queue.ack(queueName, message.id, driverName);
283
+ await TypedQueue.enqueue(queueName, toBullMQPayload(message.payload), driverName);
284
+ await TypedQueue.ack(queueName, message.id, driverName);
268
285
  return true;
269
286
  };
270
287
 
@@ -277,7 +294,7 @@ const onProcessSuccess = async <TPayload>(input: {
277
294
  startedAtMs: number;
278
295
  baseLogFields: Record<string, unknown>;
279
296
  }): Promise<boolean> => {
280
- await Queue.ack(input.queueName, input.message.id, input.driverName);
297
+ await TypedQueue.ack(input.queueName, input.message.id, input.driverName);
281
298
 
282
299
  if (typeof input.trackerApi.completed === 'function') {
283
300
  await input.trackerApi.completed({
@@ -324,10 +341,7 @@ const onProcessFailure = async <TPayload>(input: {
324
341
  if (nextAttempts < input.options.maxAttempts) {
325
342
  const retryDelayMs = getRetryDelayMs(nextAttempts);
326
343
  retryAt = new Date(Date.now() + retryDelayMs).toISOString();
327
- const currentPayload =
328
- typeof input.message.payload === 'object' && input.message.payload !== null
329
- ? (input.message.payload as Record<string, unknown>)
330
- : ({ payload: input.message.payload } as Record<string, unknown>);
344
+ const currentPayload = toBullMQPayload(input.message.payload);
331
345
 
332
346
  const payloadForRetry: BullMQPayload = {
333
347
  ...currentPayload,
@@ -335,7 +349,7 @@ const onProcessFailure = async <TPayload>(input: {
335
349
  timestamp: Date.now() + retryDelayMs,
336
350
  };
337
351
 
338
- await Queue.enqueue(input.queueName, payloadForRetry, input.driverName);
352
+ await TypedQueue.enqueue(input.queueName, payloadForRetry, input.driverName);
339
353
  Logger.info(`${input.options.kindLabel} re-queued for retry`, {
340
354
  ...input.baseLogFields,
341
355
  attempts: nextAttempts,
@@ -343,7 +357,7 @@ const onProcessFailure = async <TPayload>(input: {
343
357
  });
344
358
  }
345
359
 
346
- await Queue.ack(input.queueName, input.message.id, input.driverName);
360
+ await TypedQueue.ack(input.queueName, input.message.id, input.driverName);
347
361
  await removeHeartbeatIfSupported(input.queueName, input.message.id);
348
362
 
349
363
  if (typeof input.trackerApi.failed === 'function') {
@@ -409,7 +423,7 @@ const processQueueMessage = async <TPayload>(
409
423
  queueName: string,
410
424
  driverName?: string
411
425
  ): Promise<boolean> => {
412
- const message = (await Queue.dequeue(queueName, driverName)) as QueueMessage<TPayload> | null;
426
+ const message = await TypedQueue.dequeue<TPayload>(queueName, driverName);
413
427
  if (!message) return false;
414
428
 
415
429
  const baseLogFields = buildBaseLogFields(message, options.getLogFields);
package/src/index.ts CHANGED
@@ -71,10 +71,10 @@ export type {
71
71
  WorkerConfig,
72
72
  WorkerCostConfig,
73
73
  WorkerObservabilityConfig,
74
- WorkerStatus,
75
- WorkerVersioningConfig,
76
74
  WorkersConfigOverrides,
77
75
  WorkersGlobalConfig,
76
+ WorkerStatus,
77
+ WorkerVersioningConfig,
78
78
  } from '@zintrust/core';
79
79
 
80
80
  // Re-export bullmq types for type compatibility
@@ -97,6 +97,7 @@ export type {
97
97
  } from './ChaosEngineering';
98
98
  export type { ISLAConfig, ISLAReport, ISLAStatus, ISLAViolation, ITimeRange } from './SLAMonitor';
99
99
 
100
+ export type * from './config/workerConfig';
100
101
  export type * from './type';
101
102
 
102
103
  /**
package/src/register.ts CHANGED
@@ -15,6 +15,7 @@ type WorkerCommandsModule = {
15
15
  createWorkerStartAllCommand: () => CliCommandProvider;
16
16
  createWorkerStopCommand: () => CliCommandProvider;
17
17
  createWorkerRestartCommand: () => CliCommandProvider;
18
+ createWorkerDoctorCommand: () => CliCommandProvider;
18
19
  createWorkerSummaryCommand: () => CliCommandProvider;
19
20
  };
20
21
  };
@@ -38,6 +39,7 @@ const getWorkerProviders = (): Array<[string, CliCommandProvider]> => {
38
39
  ['worker:start-all', WorkerCommands.createWorkerStartAllCommand()],
39
40
  ['worker:stop', WorkerCommands.createWorkerStopCommand()],
40
41
  ['worker:restart', WorkerCommands.createWorkerRestartCommand()],
42
+ ['worker:doctor', WorkerCommands.createWorkerDoctorCommand()],
41
43
  ['worker:summary', WorkerCommands.createWorkerSummaryCommand()],
42
44
  ];
43
45
  };