@zintrust/workers 0.4.4 → 0.4.27

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);
@@ -127,6 +127,11 @@ type QueueWorker = {
127
127
  signal?: AbortSignal;
128
128
  maxDurationMs?: number;
129
129
  }) => Promise<number>;
130
+ __zintrustQueueWorkerMeta?: Readonly<{
131
+ kindLabel: string;
132
+ defaultQueueName: string;
133
+ maxAttempts: number;
134
+ }>;
130
135
  };
131
136
 
132
137
  export type CreateQueueWorkerOptions<TPayload> = {
@@ -404,7 +409,7 @@ const processQueueMessage = async <TPayload>(
404
409
  queueName: string,
405
410
  driverName?: string
406
411
  ): Promise<boolean> => {
407
- const message = await Queue.dequeue<TPayload>(queueName, driverName);
412
+ const message = (await Queue.dequeue(queueName, driverName)) as QueueMessage<TPayload> | null;
408
413
  if (!message) return false;
409
414
 
410
415
  const baseLogFields = buildBaseLogFields(message, options.getLogFields);
@@ -597,6 +602,17 @@ export function createQueueWorker<TPayload>(
597
602
  const processAll = createProcessAll(options.defaultQueueName, processOne);
598
603
  const runOnce = createRunOnce(options.defaultQueueName, processOne);
599
604
  const startWorker = createStartWorker(options.kindLabel, options.defaultQueueName, processOne);
605
+ const queueWorkerMeta = Object.freeze({
606
+ kindLabel: options.kindLabel,
607
+ defaultQueueName: options.defaultQueueName,
608
+ maxAttempts: options.maxAttempts,
609
+ });
600
610
 
601
- return Object.freeze({ processOne, processAll, runOnce, startWorker });
611
+ return Object.freeze({
612
+ processOne,
613
+ processAll,
614
+ runOnce,
615
+ startWorker,
616
+ __zintrustQueueWorkerMeta: queueWorkerMeta,
617
+ });
602
618
  }
@@ -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
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