@zintrust/workers 0.4.4 → 0.4.34

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.
package/src/WorkerInit.ts CHANGED
@@ -148,7 +148,7 @@ type PersistenceOverride = WorkerPersistenceConfig;
148
148
 
149
149
  type AutoStartTask = AutoStartCandidate & {
150
150
  persistenceOverride: PersistenceOverride;
151
- source: 'database' | 'redis' | 'memory';
151
+ source: 'database' | 'redis' | 'memory' | 'file';
152
152
  };
153
153
 
154
154
  const resolveAutoStartCandidates = (records: AutoStartCandidate[]): AutoStartCandidate[] => {
@@ -229,6 +229,79 @@ const collectAutoStartTasks = async (): Promise<AutoStartTask[]> => {
229
229
  return tasks;
230
230
  };
231
231
 
232
+ export const buildFileBackedAutoStartTasks = (
233
+ records: AutoStartCandidate[],
234
+ warn: (message: string) => void = Logger.warn
235
+ ): AutoStartTask[] => {
236
+ const tasks: AutoStartTask[] = [];
237
+ const seenWorkerNames = new Set<string>();
238
+ const candidates = resolveAutoStartCandidates(records);
239
+
240
+ for (const record of candidates) {
241
+ if (seenWorkerNames.has(record.name)) {
242
+ warn(
243
+ `Worker ${record.name} appears multiple times in file-backed discovery; keeping the first definition and skipping duplicates.`
244
+ );
245
+ continue;
246
+ }
247
+
248
+ seenWorkerNames.add(record.name);
249
+ tasks.push({
250
+ ...record,
251
+ persistenceOverride: { driver: 'memory' },
252
+ source: 'file',
253
+ });
254
+ }
255
+
256
+ return tasks;
257
+ };
258
+
259
+ export const selectAutoStartTasks = (
260
+ persistedTasks: AutoStartTask[],
261
+ fileRecords: AutoStartCandidate[],
262
+ warn: (message: string) => void = Logger.warn
263
+ ): AutoStartTask[] => {
264
+ if (persistedTasks.length > 0) {
265
+ return persistedTasks;
266
+ }
267
+
268
+ return buildFileBackedAutoStartTasks(fileRecords, warn);
269
+ };
270
+
271
+ export const selectAutoStartNames = (
272
+ persistedRecords: AutoStartCandidate[],
273
+ fileRecords: AutoStartCandidate[],
274
+ warn: (message: string) => void = Logger.warn
275
+ ): { names: string[]; source: 'persisted' | 'file' | 'none' } => {
276
+ const persistedNames = resolveAutoStartCandidates(persistedRecords).map((record) => record.name);
277
+
278
+ if (persistedNames.length > 0) {
279
+ return { names: persistedNames, source: 'persisted' };
280
+ }
281
+
282
+ const fileNames = buildFileBackedAutoStartTasks(fileRecords, warn).map((record) => record.name);
283
+ return { names: fileNames, source: fileNames.length > 0 ? 'file' : 'none' };
284
+ };
285
+
286
+ const collectFileBackedAutoStartTasks = async (): Promise<AutoStartTask[]> => {
287
+ try {
288
+ const records = await WorkerFactory.listFileBackedRecords();
289
+ const tasks = buildFileBackedAutoStartTasks(records);
290
+
291
+ Logger.debug('File-backed auto-start discovery', {
292
+ totalRecords: records.length,
293
+ candidateCount: tasks.length,
294
+ });
295
+
296
+ return tasks;
297
+ } catch (error) {
298
+ const message = error instanceof Error ? error.message : String(error);
299
+ Logger.warn(`File-backed auto-start discovery failed: ${message}`);
300
+ }
301
+
302
+ return [];
303
+ };
304
+
232
305
  const isWorkerTrulyRunning = async (name: string): Promise<boolean> => {
233
306
  const existing = WorkerFactory.get(name);
234
307
  if (!existing) return false;
@@ -357,16 +430,21 @@ async function autoStartPersistedWorkers(): Promise<void> {
357
430
  }
358
431
 
359
432
  try {
360
- const candidates = await collectAutoStartTasks();
433
+ let candidates = await collectAutoStartTasks();
434
+
435
+ if (candidates.length === 0) {
436
+ candidates = await collectFileBackedAutoStartTasks();
437
+ }
361
438
 
362
439
  const results = await Promise.all(candidates.map(async (record) => autoStartOneWorker(record)));
363
440
 
364
441
  const startedCount = results.filter((item) => item.started).length;
365
442
  const skippedCount = results.filter((item) => item.skipped).length;
366
- Logger.info('Auto-started persisted workers', {
443
+ Logger.info('Auto-started workers', {
367
444
  total: candidates.length,
368
445
  started: startedCount,
369
446
  skipped: skippedCount,
447
+ source: candidates[0]?.source ?? 'none',
370
448
  });
371
449
  } catch (error) {
372
450
  const message = error instanceof Error ? error.message : String(error);
@@ -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
 
@@ -127,6 +136,11 @@ type QueueWorker = {
127
136
  signal?: AbortSignal;
128
137
  maxDurationMs?: number;
129
138
  }) => Promise<number>;
139
+ __zintrustQueueWorkerMeta?: Readonly<{
140
+ kindLabel: string;
141
+ defaultQueueName: string;
142
+ maxAttempts: number;
143
+ }>;
130
144
  };
131
145
 
132
146
  export type CreateQueueWorkerOptions<TPayload> = {
@@ -147,6 +161,14 @@ const buildBaseLogFields = <TPayload>(
147
161
  };
148
162
  };
149
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
+
150
172
  type TrackerApi = {
151
173
  started?: (input: {
152
174
  queueName: string;
@@ -258,8 +280,8 @@ const checkAndRequeueIfNotDue = async <TPayload>(
258
280
  ...baseLogFields,
259
281
  dueAt: new Date(timestamp).toISOString(),
260
282
  });
261
- await Queue.enqueue(queueName, message.payload as BullMQPayload, driverName);
262
- await Queue.ack(queueName, message.id, driverName);
283
+ await TypedQueue.enqueue(queueName, toBullMQPayload(message.payload), driverName);
284
+ await TypedQueue.ack(queueName, message.id, driverName);
263
285
  return true;
264
286
  };
265
287
 
@@ -272,7 +294,7 @@ const onProcessSuccess = async <TPayload>(input: {
272
294
  startedAtMs: number;
273
295
  baseLogFields: Record<string, unknown>;
274
296
  }): Promise<boolean> => {
275
- await Queue.ack(input.queueName, input.message.id, input.driverName);
297
+ await TypedQueue.ack(input.queueName, input.message.id, input.driverName);
276
298
 
277
299
  if (typeof input.trackerApi.completed === 'function') {
278
300
  await input.trackerApi.completed({
@@ -319,10 +341,7 @@ const onProcessFailure = async <TPayload>(input: {
319
341
  if (nextAttempts < input.options.maxAttempts) {
320
342
  const retryDelayMs = getRetryDelayMs(nextAttempts);
321
343
  retryAt = new Date(Date.now() + retryDelayMs).toISOString();
322
- const currentPayload =
323
- typeof input.message.payload === 'object' && input.message.payload !== null
324
- ? (input.message.payload as Record<string, unknown>)
325
- : ({ payload: input.message.payload } as Record<string, unknown>);
344
+ const currentPayload = toBullMQPayload(input.message.payload);
326
345
 
327
346
  const payloadForRetry: BullMQPayload = {
328
347
  ...currentPayload,
@@ -330,7 +349,7 @@ const onProcessFailure = async <TPayload>(input: {
330
349
  timestamp: Date.now() + retryDelayMs,
331
350
  };
332
351
 
333
- await Queue.enqueue(input.queueName, payloadForRetry, input.driverName);
352
+ await TypedQueue.enqueue(input.queueName, payloadForRetry, input.driverName);
334
353
  Logger.info(`${input.options.kindLabel} re-queued for retry`, {
335
354
  ...input.baseLogFields,
336
355
  attempts: nextAttempts,
@@ -338,7 +357,7 @@ const onProcessFailure = async <TPayload>(input: {
338
357
  });
339
358
  }
340
359
 
341
- await Queue.ack(input.queueName, input.message.id, input.driverName);
360
+ await TypedQueue.ack(input.queueName, input.message.id, input.driverName);
342
361
  await removeHeartbeatIfSupported(input.queueName, input.message.id);
343
362
 
344
363
  if (typeof input.trackerApi.failed === 'function') {
@@ -404,7 +423,7 @@ const processQueueMessage = async <TPayload>(
404
423
  queueName: string,
405
424
  driverName?: string
406
425
  ): Promise<boolean> => {
407
- const message = await Queue.dequeue<TPayload>(queueName, driverName);
426
+ const message = await TypedQueue.dequeue<TPayload>(queueName, driverName);
408
427
  if (!message) return false;
409
428
 
410
429
  const baseLogFields = buildBaseLogFields(message, options.getLogFields);
@@ -597,6 +616,17 @@ export function createQueueWorker<TPayload>(
597
616
  const processAll = createProcessAll(options.defaultQueueName, processOne);
598
617
  const runOnce = createRunOnce(options.defaultQueueName, processOne);
599
618
  const startWorker = createStartWorker(options.kindLabel, options.defaultQueueName, processOne);
619
+ const queueWorkerMeta = Object.freeze({
620
+ kindLabel: options.kindLabel,
621
+ defaultQueueName: options.defaultQueueName,
622
+ maxAttempts: options.maxAttempts,
623
+ });
600
624
 
601
- return Object.freeze({ processOne, processAll, runOnce, startWorker });
625
+ return Object.freeze({
626
+ processOne,
627
+ processAll,
628
+ runOnce,
629
+ startWorker,
630
+ __zintrustQueueWorkerMeta: queueWorkerMeta,
631
+ });
602
632
  }
@@ -278,6 +278,10 @@ async function getWorkersFromMixedPersistence(
278
278
  ...transformToWorkerData(redisRecords, 'redis'),
279
279
  ];
280
280
 
281
+ if (workers.length === 0) {
282
+ return getWorkersFromFileFallback(limit, query.includeInactive === true);
283
+ }
284
+
281
285
  return {
282
286
  workers,
283
287
  total:
@@ -312,6 +316,11 @@ async function getWorkersFromSinglePersistence(
312
316
  { driver: normalizedDriver },
313
317
  { offset, limit, includeInactive: query.includeInactive }
314
318
  );
319
+
320
+ if (driverRecords.length === 0) {
321
+ return getWorkersFromFileFallback(limit, query.includeInactive === true);
322
+ }
323
+
315
324
  const workers = transformToWorkerData(driverRecords, normalizedDriver);
316
325
 
317
326
  return {
@@ -333,6 +342,35 @@ async function getWorkersFromSinglePersistence(
333
342
  }
334
343
  }
335
344
 
345
+ async function getWorkersFromFileFallback(
346
+ limit: number,
347
+ includeInactive: boolean
348
+ ): Promise<PersistenceResult> {
349
+ try {
350
+ const discovered = await WorkerFactory.listFileBackedRecords();
351
+ const filtered = includeInactive
352
+ ? discovered
353
+ : discovered.filter((record) => record.activeStatus !== false);
354
+
355
+ return {
356
+ workers: transformToWorkerData(filtered, 'memory'),
357
+ total: filtered.length,
358
+ drivers: getAvailableDriversFromDrivers(['memory']),
359
+ effectiveLimit: limit,
360
+ prePaginated: false,
361
+ };
362
+ } catch (error) {
363
+ Logger.debug('File-backed worker fallback failed', error);
364
+ return {
365
+ workers: [],
366
+ total: 0,
367
+ drivers: getAvailableDriversFromDrivers(['memory']),
368
+ effectiveLimit: limit,
369
+ prePaginated: false,
370
+ };
371
+ }
372
+ }
373
+
336
374
  const normalizeDriver = (driver: string): WorkerDriver => {
337
375
  if (driver === 'db' || driver === 'database') return 'database';
338
376
  if (driver === 'redis') return 'redis';
@@ -527,17 +565,17 @@ async function getQueueData(): Promise<QueueData> {
527
565
  // Get queue statistics based on QUEUE_DRIVER
528
566
  switch (queueDriver) {
529
567
  case 'redis':
530
- return getRedisQueueData();
568
+ return await getRedisQueueData();
531
569
  case 'database':
532
- return getDatabaseQueueData();
570
+ return await getDatabaseQueueData();
533
571
  case 'db':
534
- return getDatabaseQueueData();
572
+ return await getDatabaseQueueData();
535
573
  default:
536
- return getMemoryQueueData();
574
+ return await getMemoryQueueData();
537
575
  }
538
576
  } catch (error) {
539
577
  Logger.error('Error fetching queue data:', error);
540
- return getMemoryQueueData();
578
+ return await getMemoryQueueData();
541
579
  }
542
580
  }
543
581
 
@@ -603,7 +641,12 @@ async function getDatabaseQueueData(): Promise<QueueData> {
603
641
  const db = await useEnsureDbConnected();
604
642
 
605
643
  // Get queue statistics from actual database tables using proper query builder
606
- const queueStats = (await db
644
+ const queueStats: {
645
+ totalQueues: number;
646
+ totalJobs: number;
647
+ processingJobs: number;
648
+ failedJobs: number;
649
+ } | null = await db
607
650
  .table('queue_jobs')
608
651
  .select('COUNT(DISTINCT queue) as totalQueues')
609
652
  .selectAs('COUNT(*)', 'totalJobs')
@@ -612,12 +655,7 @@ async function getDatabaseQueueData(): Promise<QueueData> {
612
655
  'processingJobs'
613
656
  )
614
657
  .selectAs('SUM(CASE WHEN failed_at IS NOT NULL THEN 1 ELSE 0 END)', 'failedJobs')
615
- .first()) as {
616
- totalQueues: number;
617
- totalJobs: number;
618
- processingJobs: number;
619
- failedJobs: number;
620
- } | null;
658
+ .first();
621
659
 
622
660
  const stats = queueStats || {
623
661
  totalQueues: 0,
@@ -733,7 +771,9 @@ async function enrichWithDetails(workers: WorkerData[]): Promise<WorkerData[]> {
733
771
  async function buildWorkerDetails(worker: WorkerData): Promise<WorkerData> {
734
772
  try {
735
773
  const persistenceOverride = resolvePersistenceOverride(worker.driver);
736
- const persisted = await WorkerFactory.getPersisted(worker.name, persistenceOverride);
774
+ const persisted =
775
+ (await WorkerFactory.getPersisted(worker.name, persistenceOverride)) ??
776
+ (await WorkerFactory.getFileBackedRecord(worker.name));
737
777
  const health = await getWorkerHealthSnapshot(worker.name, worker.health);
738
778
  const metrics = await getWorkerMetricsSnapshot(worker.name, worker);
739
779
  const configuration = buildWorkerConfiguration(worker, persisted);
@@ -896,6 +936,13 @@ export async function getWorkerDetails(name: string, driver?: string): Promise<W
896
936
  }
897
937
  }
898
938
 
939
+ if (!worker) {
940
+ const fileBacked = await WorkerFactory.getFileBackedRecord(name);
941
+ if (fileBacked) {
942
+ worker = buildWorkerFromRecord(fileBacked, 'memory');
943
+ }
944
+ }
945
+
899
946
  if (!worker) {
900
947
  throw ErrorFactory.createWorkerError(`Worker ${name} not found`);
901
948
  }
package/src/index.ts CHANGED
@@ -44,7 +44,12 @@ export type {
44
44
  WorkerFactoryConfig,
45
45
  WorkerPersistenceConfig,
46
46
  } from './WorkerFactory';
47
- export { WorkerInit } from './WorkerInit';
47
+ export {
48
+ buildFileBackedAutoStartTasks,
49
+ selectAutoStartNames,
50
+ selectAutoStartTasks,
51
+ WorkerInit,
52
+ } from './WorkerInit';
48
53
  export { WorkerShutdown } from './WorkerShutdown';
49
54
 
50
55
  // HTTP Controllers & Routes
@@ -66,10 +71,10 @@ export type {
66
71
  WorkerConfig,
67
72
  WorkerCostConfig,
68
73
  WorkerObservabilityConfig,
69
- WorkerStatus,
70
- WorkerVersioningConfig,
71
74
  WorkersConfigOverrides,
72
75
  WorkersGlobalConfig,
76
+ WorkerStatus,
77
+ WorkerVersioningConfig,
73
78
  } from '@zintrust/core';
74
79
 
75
80
  // Re-export bullmq types for type compatibility
@@ -92,6 +97,7 @@ export type {
92
97
  } from './ChaosEngineering';
93
98
  export type { ISLAConfig, ISLAReport, ISLAStatus, ISLAViolation, ITimeRange } from './SLAMonitor';
94
99
 
100
+ export type * from './config/workerConfig';
95
101
  export type * from './type';
96
102
 
97
103
  /**
package/src/register.ts CHANGED
@@ -1,12 +1,12 @@
1
+ type Registry = {
2
+ register: (id: string, provider: CliCommandProvider) => void;
3
+ };
4
+
1
5
  type CliCommandProvider = {
2
6
  getCommand: () => unknown;
3
7
  name?: string;
4
8
  };
5
9
 
6
- type Registry = {
7
- register: (id: string, provider: CliCommandProvider) => void;
8
- };
9
-
10
10
  type WorkerCommandsModule = {
11
11
  WorkerCommands: {
12
12
  createWorkerListCommand: () => CliCommandProvider;
@@ -20,7 +20,12 @@ type WorkerCommandsModule = {
20
20
  };
21
21
 
22
22
  const commandModule = (await (async (): Promise<WorkerCommandsModule> => {
23
- return (await import('@zintrust/core/cli')) as unknown as WorkerCommandsModule;
23
+ const workerCommandsSpecifier = '@zintrust/core/worker-commands';
24
+ try {
25
+ return (await import(workerCommandsSpecifier)) as unknown as WorkerCommandsModule;
26
+ } catch {
27
+ return (await import('@zintrust/core/cli')) as unknown as WorkerCommandsModule;
28
+ }
24
29
  })()) satisfies WorkerCommandsModule;
25
30
 
26
31
  const getWorkerProviders = (): Array<[string, CliCommandProvider]> => {
@@ -59,12 +64,12 @@ registerWorkerCliCommands({
59
64
  });
60
65
 
61
66
  try {
62
- const core = (await import('@zintrust/core/cli')) as unknown as {
67
+ const coreCli = (await import('@zintrust/core/cli')) as unknown as {
63
68
  OptionalCliCommandRegistry?: Registry;
64
69
  };
65
70
 
66
- if (core.OptionalCliCommandRegistry !== undefined) {
67
- registerWorkerCliCommands(core.OptionalCliCommandRegistry);
71
+ if (coreCli.OptionalCliCommandRegistry !== undefined) {
72
+ registerWorkerCliCommands(coreCli.OptionalCliCommandRegistry);
68
73
  }
69
74
  } catch {
70
75
  // no-op