@zintrust/workers 0.4.3 → 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/README.md CHANGED
@@ -659,6 +659,27 @@ registerWorkerRoutes(Router);
659
659
  Processor specs can be file paths or URL specs (recommended for production). Remote processors
660
660
  must export a named `ZinTrustProcessor` function.
661
661
 
662
+ For file-backed worker discovery, export a `workerDefinition` object from the worker module. This
663
+ lets fresh projects surface worker metadata before any persistence record exists.
664
+
665
+ ```typescript
666
+ export const workerDefinition = Object.freeze({
667
+ name: 'example-worker',
668
+ queueName: 'example-worker',
669
+ version: '1.0.0',
670
+ autoStart: false,
671
+ activeStatus: true,
672
+ concurrency: 1,
673
+ processorSpec: 'app/Workers/ExampleWorker.ts',
674
+ });
675
+
676
+ export async function ZinTrustProcessor(payload: unknown): Promise<void> {
677
+ return undefined;
678
+ }
679
+
680
+ export default ZinTrustProcessor;
681
+ ```
682
+
662
683
  Workers support `activeStatus` to pause without deletion; inactive workers do not auto-start.
663
684
 
664
685
  See the [API Reference](#api-reference) section for all available endpoints.
@@ -683,6 +704,11 @@ await WorkerInit.initialize({
683
704
  });
684
705
  ```
685
706
 
707
+ When `WORKER_AUTO_START=true`, ZinTrust first auto-starts persisted workers. If persisted discovery
708
+ finds no auto-start candidates, `WorkerInit.autoStartPersistedWorkers()` falls back to file-backed
709
+ worker definitions discovered from project files. This keeps existing persisted deployments stable
710
+ while allowing fresh apps to boot workers from code-first definitions.
711
+
686
712
  ### Graceful Shutdown
687
713
 
688
714
  Shutdown workers gracefully:
@@ -19,5 +19,10 @@ export declare const BroadcastWorker: Readonly<{
19
19
  signal?: AbortSignal;
20
20
  maxDurationMs?: number;
21
21
  }) => Promise<number>;
22
+ __zintrustQueueWorkerMeta?: Readonly<{
23
+ kindLabel: string;
24
+ defaultQueueName: string;
25
+ maxAttempts: number;
26
+ }>;
22
27
  }>;
23
28
  export default BroadcastWorker;
@@ -19,5 +19,10 @@ export declare const NotificationWorker: Readonly<{
19
19
  signal?: AbortSignal;
20
20
  maxDurationMs?: number;
21
21
  }) => Promise<number>;
22
+ __zintrustQueueWorkerMeta?: Readonly<{
23
+ kindLabel: string;
24
+ defaultQueueName: string;
25
+ maxAttempts: number;
26
+ }>;
22
27
  }>;
23
28
  export default NotificationWorker;
@@ -188,6 +188,14 @@ export declare const WorkerFactory: Readonly<{
188
188
  * Start a worker from persisted storage when it is not registered.
189
189
  */
190
190
  startFromPersisted(name: string, persistenceOverride?: WorkerPersistenceConfig): Promise<void>;
191
+ /**
192
+ * List worker definitions discovered from project files.
193
+ */
194
+ listFileBackedRecords(): Promise<WorkerRecord[]>;
195
+ /**
196
+ * Get a file-backed worker definition by name.
197
+ */
198
+ getFileBackedRecord(name: string): Promise<WorkerRecord | null>;
191
199
  /**
192
200
  * Get persisted worker record
193
201
  */
@@ -3,7 +3,7 @@
3
3
  * Central factory for creating workers with all advanced features
4
4
  * Sealed namespace for immutability
5
5
  */
6
- import { Cloudflare, createRedisConnection, databaseConfig, Env, ErrorFactory, generateUuid, getBullMQSafeQueueName, JobStateTracker, Logger, NodeSingletons, queueConfig, registerDatabasesFromRuntimeConfig, useEnsureDbConnected, workersConfig, ZintrustLang, } from '@zintrust/core';
6
+ import { Cloudflare, createRedisConnection, databaseConfig, Env, ErrorFactory, generateUuid, getBullMQSafeQueueName, isFunction, isNonEmptyString, isObject, JobStateTracker, Logger, NodeSingletons, queueConfig, registerDatabasesFromRuntimeConfig, useEnsureDbConnected, workersConfig, ZintrustLang, } from '@zintrust/core';
7
7
  import { Worker } from 'bullmq';
8
8
  import { AutoScaler } from './AutoScaler.js';
9
9
  import { CanaryController } from './CanaryController.js';
@@ -241,6 +241,24 @@ const processorRegistry = new Map();
241
241
  const processorPathRegistry = new Map();
242
242
  const processorResolvers = [];
243
243
  const processorSpecRegistry = new Map();
244
+ const queueWorkerMetaKey = '__zintrustQueueWorkerMeta';
245
+ const fileWorkerDefinitionExportKeys = Object.freeze([
246
+ 'workerDefinition',
247
+ 'workerConfig',
248
+ 'zintrustWorker',
249
+ 'ZinTrustWorker',
250
+ 'worker',
251
+ 'defaultWorkerDefinition',
252
+ ]);
253
+ const workerDiscoveryDirectories = Object.freeze([
254
+ ['dist', 'app', 'Workers'],
255
+ ['dist', 'src', 'workers'],
256
+ ['dist', 'src', 'Workers'],
257
+ ['app', 'Workers'],
258
+ ['src', 'workers'],
259
+ ['src', 'Workers'],
260
+ ]);
261
+ const workerDiscoveryExtensions = new Set(['.js', '.mjs', '.cjs', '.ts']);
244
262
  const processorCache = new Map();
245
263
  let processorCacheSize = 0;
246
264
  const buildPersistenceBootstrapConfig = () => {
@@ -311,6 +329,270 @@ const parseCacheControl = (value) => {
311
329
  return Number.isFinite(parsed) ? { maxAge: parsed } : {};
312
330
  };
313
331
  const getProcessorSpecConfig = () => workersConfig.processorSpec;
332
+ const toPosixPath = (value) => value.split(path.sep).join('/');
333
+ 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();
341
+ };
342
+ const supportsWorkerFileDiscovery = () => {
343
+ return (isNodeRuntime() &&
344
+ canUseProjectFileImports() &&
345
+ typeof NodeSingletons.fs.readdirSync === 'function' &&
346
+ typeof NodeSingletons.fs.statSync === 'function');
347
+ };
348
+ const isSupportedWorkerModuleFile = (fileName) => {
349
+ if (!isNonEmptyString(fileName))
350
+ return false;
351
+ if (fileName.endsWith('.d.ts'))
352
+ return false;
353
+ if (fileName.includes('.test.') || fileName.includes('.spec.'))
354
+ return false;
355
+ return workerDiscoveryExtensions.has(path.extname(fileName));
356
+ };
357
+ const getWorkerDiscoveryDirectories = () => {
358
+ if (!supportsWorkerFileDiscovery())
359
+ return [];
360
+ const root = resolveProjectRoot();
361
+ return workerDiscoveryDirectories.map((segments) => path.join(root, ...segments));
362
+ };
363
+ const listWorkerDefinitionFiles = () => {
364
+ if (!supportsWorkerFileDiscovery())
365
+ return [];
366
+ const discovered = new Set();
367
+ for (const directory of getWorkerDiscoveryDirectories()) {
368
+ try {
369
+ if (!NodeSingletons.fs.existsSync(directory))
370
+ continue;
371
+ const stats = NodeSingletons.fs.statSync(directory);
372
+ if (!stats.isDirectory())
373
+ continue;
374
+ const entries = NodeSingletons.fs.readdirSync(directory, { withFileTypes: true });
375
+ for (const entry of entries) {
376
+ if (!entry.isFile() || !isSupportedWorkerModuleFile(entry.name))
377
+ continue;
378
+ discovered.add(path.join(directory, entry.name));
379
+ }
380
+ }
381
+ catch (error) {
382
+ Logger.debug(`Worker file discovery failed for directory: ${directory}`, error);
383
+ }
384
+ }
385
+ return Array.from(discovered);
386
+ };
387
+ const getProjectRelativeWorkerSpec = (sourcePath) => {
388
+ const relativePath = path.relative(resolveProjectRoot(), sourcePath);
389
+ return isNonEmptyString(relativePath) ? toPosixPath(relativePath) : toPosixPath(sourcePath);
390
+ };
391
+ const isQueueWorkerMeta = (value) => {
392
+ return (isObject(value) &&
393
+ isNonEmptyString(value['kindLabel']) &&
394
+ isNonEmptyString(value['defaultQueueName']) &&
395
+ typeof value['maxAttempts'] === 'number');
396
+ };
397
+ const getExportedQueueWorkerMeta = (mod) => {
398
+ for (const value of Object.values(mod)) {
399
+ if (!isObject(value))
400
+ continue;
401
+ const meta = value[queueWorkerMetaKey];
402
+ if (isQueueWorkerMeta(meta))
403
+ return meta;
404
+ }
405
+ return undefined;
406
+ };
407
+ const assignStringDefinitionField = (definition, key, value) => {
408
+ if (isNonEmptyString(value)) {
409
+ definition[key] = value;
410
+ }
411
+ };
412
+ const assignBooleanDefinitionField = (definition, key, value) => {
413
+ if (typeof value === 'boolean') {
414
+ definition[key] = value;
415
+ }
416
+ };
417
+ const assignObjectDefinitionField = (definition, key, value) => {
418
+ if (isObject(value)) {
419
+ definition[key] = value;
420
+ }
421
+ };
422
+ const assignConcurrencyDefinitionField = (definition, value) => {
423
+ if (typeof value === 'number' && Number.isFinite(value)) {
424
+ definition.concurrency = Math.max(1, Math.floor(value));
425
+ }
426
+ };
427
+ const assignProcessorDefinitionField = (definition, value) => {
428
+ if (isFunction(value)) {
429
+ definition.processor = value;
430
+ }
431
+ };
432
+ const normalizeFileWorkerDefinition = (value) => {
433
+ if (!isObject(value))
434
+ return undefined;
435
+ const definition = {};
436
+ assignStringDefinitionField(definition, 'name', value['name']);
437
+ assignStringDefinitionField(definition, 'queueName', value['queueName']);
438
+ assignStringDefinitionField(definition, 'version', value['version']);
439
+ assignStringDefinitionField(definition, 'status', value['status']);
440
+ assignStringDefinitionField(definition, 'region', value['region']);
441
+ assignStringDefinitionField(definition, 'processorSpec', value['processorSpec']);
442
+ assignBooleanDefinitionField(definition, 'autoStart', value['autoStart']);
443
+ assignBooleanDefinitionField(definition, 'activeStatus', value['activeStatus']);
444
+ assignObjectDefinitionField(definition, 'features', value['features']);
445
+ assignObjectDefinitionField(definition, 'infrastructure', value['infrastructure']);
446
+ assignObjectDefinitionField(definition, 'datacenter', value['datacenter']);
447
+ assignConcurrencyDefinitionField(definition, value['concurrency']);
448
+ assignProcessorDefinitionField(definition, value['processor']);
449
+ return definition;
450
+ };
451
+ const getExportedFileWorkerDefinition = (mod) => {
452
+ for (const key of fileWorkerDefinitionExportKeys) {
453
+ const normalized = normalizeFileWorkerDefinition(mod[key]);
454
+ if (normalized)
455
+ return normalized;
456
+ }
457
+ const defaultExport = mod['default'];
458
+ if (!isFunction(defaultExport)) {
459
+ const normalized = normalizeFileWorkerDefinition(defaultExport);
460
+ if (normalized)
461
+ return normalized;
462
+ }
463
+ return undefined;
464
+ };
465
+ const importWorkerDefinitionModule = async (sourcePath) => {
466
+ if (!supportsWorkerFileDiscovery())
467
+ return undefined;
468
+ try {
469
+ return (await import(NodeSingletons.url.pathToFileURL(sourcePath).href));
470
+ }
471
+ catch (error) {
472
+ Logger.debug(`Failed to import worker definition module: ${sourcePath}`, error);
473
+ return undefined;
474
+ }
475
+ };
476
+ const resolveDiscoveredWorkerProcessor = (definition, mod, sourcePath) => {
477
+ if (definition?.processor)
478
+ return definition.processor;
479
+ return extractZinTrustProcessor(mod, sourcePath) ?? pickProcessorFromModule(mod, sourcePath);
480
+ };
481
+ const resolveDiscoveredWorkerName = (definition, sourcePath) => {
482
+ return (definition?.name ?? normalizeWorkerFileName(path.basename(sourcePath, path.extname(sourcePath))));
483
+ };
484
+ const resolveDiscoveredQueueName = (definition, queueWorkerMeta, recordName) => {
485
+ if (definition?.queueName)
486
+ return definition.queueName;
487
+ if (queueWorkerMeta?.defaultQueueName)
488
+ return queueWorkerMeta.defaultQueueName;
489
+ return `${recordName}-queue`;
490
+ };
491
+ const resolveDiscoveredProcessorSpec = (definition, sourcePath) => {
492
+ return definition?.processorSpec ?? getProjectRelativeWorkerSpec(sourcePath);
493
+ };
494
+ const resolveDiscoveredVersion = (definition) => {
495
+ return definition?.version ?? '1.0.0';
496
+ };
497
+ const resolveDiscoveredStatus = (definition) => {
498
+ return definition?.status ?? WorkerCreationStatus.STOPPED;
499
+ };
500
+ const resolveDiscoveredAutoStart = (definition) => {
501
+ return definition?.autoStart ?? false;
502
+ };
503
+ const resolveDiscoveredConcurrency = (definition) => {
504
+ return definition?.concurrency ?? 1;
505
+ };
506
+ const resolveDiscoveredActiveStatus = (definition) => {
507
+ return definition?.activeStatus ?? true;
508
+ };
509
+ const buildDiscoveredWorkerRecord = (definition, queueWorkerMeta, sourcePath) => {
510
+ const recordName = resolveDiscoveredWorkerName(definition, sourcePath);
511
+ if (!isNonEmptyString(recordName))
512
+ return undefined;
513
+ return {
514
+ name: recordName,
515
+ queueName: resolveDiscoveredQueueName(definition, queueWorkerMeta, recordName),
516
+ version: resolveDiscoveredVersion(definition),
517
+ status: resolveDiscoveredStatus(definition),
518
+ autoStart: resolveDiscoveredAutoStart(definition),
519
+ concurrency: resolveDiscoveredConcurrency(definition),
520
+ region: definition?.region ?? null,
521
+ processorSpec: resolveDiscoveredProcessorSpec(definition, sourcePath),
522
+ activeStatus: resolveDiscoveredActiveStatus(definition),
523
+ features: definition?.features ?? null,
524
+ infrastructure: definition?.infrastructure ?? null,
525
+ datacenter: definition?.datacenter ?? null,
526
+ createdAt: new Date(),
527
+ updatedAt: new Date(),
528
+ };
529
+ };
530
+ const buildDiscoveredWorker = (mod, sourcePath) => {
531
+ const definition = getExportedFileWorkerDefinition(mod);
532
+ const queueWorkerMeta = getExportedQueueWorkerMeta(mod);
533
+ const processor = resolveDiscoveredWorkerProcessor(definition, mod, sourcePath);
534
+ const record = buildDiscoveredWorkerRecord(definition, queueWorkerMeta, sourcePath);
535
+ if (!record)
536
+ return undefined;
537
+ return {
538
+ record,
539
+ processor,
540
+ sourcePath,
541
+ };
542
+ };
543
+ const resolveStartFromPersistedRecord = async (name, persistenceOverride) => {
544
+ const persistedRecord = await getPersistedRecord(name, persistenceOverride);
545
+ const discovered = persistedRecord ? null : await getDiscoveredFileWorker(name);
546
+ const effectiveRecord = persistedRecord ?? discovered?.record ?? null;
547
+ if (!effectiveRecord) {
548
+ throw ErrorFactory.createNotFoundError(`Worker "${name}" not found in persistence store`);
549
+ }
550
+ return { record: effectiveRecord, discovered };
551
+ };
552
+ const resolveStartFromPersistedProcessor = async (name, record, discovered) => {
553
+ let processor = await resolveProcessor(name);
554
+ if (!processor && discovered?.processor) {
555
+ processor = discovered.processor;
556
+ }
557
+ const spec = record.processorSpec ?? undefined;
558
+ if (!processor && spec) {
559
+ try {
560
+ processor = await resolveProcessorSpec(spec);
561
+ }
562
+ catch (error) {
563
+ Logger.error(`Failed to resolve processor module for "${name}"`, error);
564
+ }
565
+ }
566
+ if (!processor) {
567
+ throw ErrorFactory.createConfigError(`Worker "${name}" processor is not registered or resolvable. Register the processor at startup or persist a processorSpec.`);
568
+ }
569
+ return processor;
570
+ };
571
+ const discoverFileBackedWorkers = async () => {
572
+ const files = listWorkerDefinitionFiles();
573
+ if (files.length === 0)
574
+ return [];
575
+ const discovered = new Map();
576
+ for (const filePath of files) {
577
+ // eslint-disable-next-line no-await-in-loop
578
+ const mod = await importWorkerDefinitionModule(filePath);
579
+ if (!mod)
580
+ continue;
581
+ const discoveredWorker = buildDiscoveredWorker(mod, filePath);
582
+ if (!discoveredWorker)
583
+ continue;
584
+ if (discovered.has(discoveredWorker.record.name)) {
585
+ Logger.warn(`Duplicate file-backed worker definition detected for "${discoveredWorker.record.name}". Keeping the first discovered module.`);
586
+ continue;
587
+ }
588
+ discovered.set(discoveredWorker.record.name, discoveredWorker);
589
+ }
590
+ return Array.from(discovered.values());
591
+ };
592
+ const getDiscoveredFileWorker = async (name) => {
593
+ const discovered = await discoverFileBackedWorkers();
594
+ return discovered.find((entry) => entry.record.name === name) ?? null;
595
+ };
314
596
  const computeSha256 = async (value) => {
315
597
  if (typeof globalThis !== 'undefined' && globalThis.crypto?.subtle) {
316
598
  const data = new TextEncoder().encode(value);
@@ -384,7 +666,7 @@ const waitForWorkerConnection = async (worker, name, _queueName, timeoutMs) => {
384
666
  const checkConnection = async () => {
385
667
  try {
386
668
  // Check if worker is actually running
387
- const isRunning = await worker.isRunning();
669
+ const isRunning = await worker.isRunning(); // NOSONAR - BullMQ's isRunning method
388
670
  if (!isRunning) {
389
671
  throw ErrorFactory.createWorkerError('Worker not running');
390
672
  }
@@ -1082,12 +1364,13 @@ const resolveRedisConfigFromEnv = (config, context) => {
1082
1364
  };
1083
1365
  const resolveRedisConfigFromDirect = (config, context) => {
1084
1366
  const fallbackDb = Env.getInt('REDIS_QUEUE_DB', ZintrustLang.REDIS_DEFAULT_DB);
1367
+ const redisConfigWithDatabase = config;
1085
1368
  let normalizedDb = fallbackDb;
1086
1369
  if (typeof config.db === 'number') {
1087
1370
  normalizedDb = config.db;
1088
1371
  }
1089
- else if (typeof config.database === 'number') {
1090
- normalizedDb = config.database;
1372
+ else if (typeof redisConfigWithDatabase.database === 'number') {
1373
+ normalizedDb = redisConfigWithDatabase.database;
1091
1374
  }
1092
1375
  return {
1093
1376
  host: requireRedisHost(config.host, context),
@@ -1398,12 +1681,8 @@ const resolveAutoScalerConfig = (input) => {
1398
1681
  };
1399
1682
  const resolveWorkerOptions = (config, autoStart) => {
1400
1683
  const options = config.options ? { ...config.options } : {};
1401
- if (options.prefix === undefined) {
1402
- options.prefix = getBullMQSafeQueueName();
1403
- }
1404
- if (options.autorun === undefined) {
1405
- options.autorun = autoStart;
1406
- }
1684
+ options.prefix ??= getBullMQSafeQueueName();
1685
+ options.autorun ??= autoStart;
1407
1686
  if (options.connection)
1408
1687
  return options;
1409
1688
  const redisConfig = resolveRedisConfigWithFallback(config.infrastructure?.redis, undefined, 'Worker requires a connection. Provide options.connection or infrastructure.redis config', 'infrastructure.redis');
@@ -2098,26 +2377,11 @@ export const WorkerFactory = Object.freeze({
2098
2377
  * Start a worker from persisted storage when it is not registered.
2099
2378
  */
2100
2379
  async startFromPersisted(name, persistenceOverride) {
2101
- const record = await getPersistedRecord(name, persistenceOverride);
2102
- if (!record) {
2103
- throw ErrorFactory.createNotFoundError(`Worker "${name}" not found in persistence store`);
2104
- }
2380
+ const { record, discovered } = await resolveStartFromPersistedRecord(name, persistenceOverride);
2105
2381
  if (record.activeStatus === false) {
2106
2382
  throw ErrorFactory.createConfigError(`Worker "${name}" is inactive`);
2107
2383
  }
2108
- let processor = await resolveProcessor(name);
2109
- const spec = record.processorSpec ?? undefined;
2110
- if (!processor && spec) {
2111
- try {
2112
- processor = await resolveProcessorSpec(spec);
2113
- }
2114
- catch (error) {
2115
- Logger.error(`Failed to resolve processor module for "${name}"`, error);
2116
- }
2117
- }
2118
- if (!processor) {
2119
- throw ErrorFactory.createConfigError(`Worker "${name}" processor is not registered or resolvable. Register the processor at startup or persist a processorSpec.`);
2120
- }
2384
+ const processor = await resolveStartFromPersistedProcessor(name, record, discovered);
2121
2385
  await WorkerFactory.create({
2122
2386
  name: record.name,
2123
2387
  queueName: record.queueName,
@@ -2132,6 +2396,20 @@ export const WorkerFactory = Object.freeze({
2132
2396
  datacenter: record.datacenter,
2133
2397
  });
2134
2398
  },
2399
+ /**
2400
+ * List worker definitions discovered from project files.
2401
+ */
2402
+ async listFileBackedRecords() {
2403
+ const discovered = await discoverFileBackedWorkers();
2404
+ return discovered.map((entry) => entry.record);
2405
+ },
2406
+ /**
2407
+ * Get a file-backed worker definition by name.
2408
+ */
2409
+ async getFileBackedRecord(name) {
2410
+ const discovered = await getDiscoveredFileWorker(name);
2411
+ return discovered?.record ?? null;
2412
+ },
2135
2413
  /**
2136
2414
  * Get persisted worker record
2137
2415
  */
@@ -7,6 +7,7 @@
7
7
  * - Sets up auto-scaling and health checks
8
8
  * - Ensures graceful startup and shutdown
9
9
  */
10
+ import type { WorkerPersistenceConfig } from './WorkerFactory';
10
11
  export interface IWorkerInitOptions {
11
12
  /**
12
13
  * Whether to start resource monitoring on initialization
@@ -42,6 +43,22 @@ interface IInitState {
42
43
  autoScaling: boolean;
43
44
  shutdownHandlersRegistered: boolean;
44
45
  }
46
+ type AutoStartCandidate = {
47
+ name: string;
48
+ autoStart: boolean;
49
+ activeStatus?: boolean;
50
+ };
51
+ type PersistenceOverride = WorkerPersistenceConfig;
52
+ type AutoStartTask = AutoStartCandidate & {
53
+ persistenceOverride: PersistenceOverride;
54
+ source: 'database' | 'redis' | 'memory' | 'file';
55
+ };
56
+ export declare const buildFileBackedAutoStartTasks: (records: AutoStartCandidate[], warn?: (message: string) => void) => AutoStartTask[];
57
+ export declare const selectAutoStartTasks: (persistedTasks: AutoStartTask[], fileRecords: AutoStartCandidate[], warn?: (message: string) => void) => AutoStartTask[];
58
+ export declare const selectAutoStartNames: (persistedRecords: AutoStartCandidate[], fileRecords: AutoStartCandidate[], warn?: (message: string) => void) => {
59
+ names: string[];
60
+ source: "persisted" | "file" | "none";
61
+ };
45
62
  /**
46
63
  * Initialize the worker management system
47
64
  */
@@ -141,6 +141,54 @@ const collectAutoStartTasks = async () => {
141
141
  }
142
142
  return tasks;
143
143
  };
144
+ export const buildFileBackedAutoStartTasks = (records, warn = Logger.warn) => {
145
+ const tasks = [];
146
+ const seenWorkerNames = new Set();
147
+ const candidates = resolveAutoStartCandidates(records);
148
+ for (const record of candidates) {
149
+ if (seenWorkerNames.has(record.name)) {
150
+ warn(`Worker ${record.name} appears multiple times in file-backed discovery; keeping the first definition and skipping duplicates.`);
151
+ continue;
152
+ }
153
+ seenWorkerNames.add(record.name);
154
+ tasks.push({
155
+ ...record,
156
+ persistenceOverride: { driver: 'memory' },
157
+ source: 'file',
158
+ });
159
+ }
160
+ return tasks;
161
+ };
162
+ export const selectAutoStartTasks = (persistedTasks, fileRecords, warn = Logger.warn) => {
163
+ if (persistedTasks.length > 0) {
164
+ return persistedTasks;
165
+ }
166
+ return buildFileBackedAutoStartTasks(fileRecords, warn);
167
+ };
168
+ export const selectAutoStartNames = (persistedRecords, fileRecords, warn = Logger.warn) => {
169
+ const persistedNames = resolveAutoStartCandidates(persistedRecords).map((record) => record.name);
170
+ if (persistedNames.length > 0) {
171
+ return { names: persistedNames, source: 'persisted' };
172
+ }
173
+ const fileNames = buildFileBackedAutoStartTasks(fileRecords, warn).map((record) => record.name);
174
+ return { names: fileNames, source: fileNames.length > 0 ? 'file' : 'none' };
175
+ };
176
+ const collectFileBackedAutoStartTasks = async () => {
177
+ try {
178
+ const records = await WorkerFactory.listFileBackedRecords();
179
+ const tasks = buildFileBackedAutoStartTasks(records);
180
+ Logger.debug('File-backed auto-start discovery', {
181
+ totalRecords: records.length,
182
+ candidateCount: tasks.length,
183
+ });
184
+ return tasks;
185
+ }
186
+ catch (error) {
187
+ const message = error instanceof Error ? error.message : String(error);
188
+ Logger.warn(`File-backed auto-start discovery failed: ${message}`);
189
+ }
190
+ return [];
191
+ };
144
192
  const isWorkerTrulyRunning = async (name) => {
145
193
  const existing = WorkerFactory.get(name);
146
194
  if (!existing)
@@ -238,14 +286,18 @@ async function autoStartPersistedWorkers() {
238
286
  return;
239
287
  }
240
288
  try {
241
- const candidates = await collectAutoStartTasks();
289
+ let candidates = await collectAutoStartTasks();
290
+ if (candidates.length === 0) {
291
+ candidates = await collectFileBackedAutoStartTasks();
292
+ }
242
293
  const results = await Promise.all(candidates.map(async (record) => autoStartOneWorker(record)));
243
294
  const startedCount = results.filter((item) => item.started).length;
244
295
  const skippedCount = results.filter((item) => item.skipped).length;
245
- Logger.info('Auto-started persisted workers', {
296
+ Logger.info('Auto-started workers', {
246
297
  total: candidates.length,
247
298
  started: startedCount,
248
299
  skipped: skippedCount,
300
+ source: candidates[0]?.source ?? 'none',
249
301
  });
250
302
  }
251
303
  catch (error) {
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "@zintrust/workers",
3
3
  "version": "0.1.52",
4
- "buildDate": "2026-03-20T13:12:38.239Z",
4
+ "buildDate": "2026-03-26T09:12:29.177Z",
5
5
  "buildEnvironment": {
6
- "node": "v22.22.1",
6
+ "node": "v25.6.1",
7
7
  "platform": "darwin",
8
8
  "arch": "arm64"
9
9
  },
10
10
  "git": {
11
- "commit": "e0e7e31c",
12
- "branch": "release"
11
+ "commit": "597f453f",
12
+ "branch": "dev"
13
13
  },
14
14
  "package": {
15
15
  "engines": {
@@ -231,8 +231,8 @@
231
231
  "sha256": "8af20d462270e7044c6ea983821f5b6e6ce8a5caf39b6e8fefff07c9a0bf071e"
232
232
  },
233
233
  "build-manifest.json": {
234
- "size": 19608,
235
- "sha256": "6940ae9e49a40ebedc6da2d5c5254caf9132267bea4308ad60686af9734c4ade"
234
+ "size": 19603,
235
+ "sha256": "ee6386bb995df82724f76c33f1333c0f77863bff9c3cb6007ac2fd20969dec90"
236
236
  },
237
237
  "config/workerConfig.d.ts": {
238
238
  "size": 132,
@@ -415,16 +415,16 @@
415
415
  "sha256": "6943403aba7442451a5abd3eb64a12d2842bb310360477b5a7180cf3b4373efa"
416
416
  },
417
417
  "index.js": {
418
- "size": 2223,
419
- "sha256": "8dfa34f1a080719f5bf54588d84c611f0dc0505bc5d73dbdcf1233d65f846e61"
418
+ "size": 2261,
419
+ "sha256": "afd4bf4715321d1e073366a7cb93ac17a1f9f647500ac4027102de65f26bc213"
420
420
  },
421
421
  "register.d.ts": {
422
- "size": 237,
423
- "sha256": "f1ee8d8a8a3be3a051ddc0e5bd2f39a11e493754a9c21719652916e85e32e202"
422
+ "size": 256,
423
+ "sha256": "07753654e043fd8ac0d42c4d74a26406929fc076c8c86b3d0513898c7da0d0aa"
424
424
  },
425
425
  "register.js": {
426
- "size": 1382,
427
- "sha256": "ad370664974adbc260713d85749a835d3ff24e94827cdfc001304773e8887b55"
426
+ "size": 1555,
427
+ "sha256": "993d453ba69ea637d3674684032fc6dd47755c23c491920c04d12e006290cfe6"
428
428
  },
429
429
  "routes/workers.d.ts": {
430
430
  "size": 498,
@@ -13,6 +13,11 @@ type QueueWorker = {
13
13
  signal?: AbortSignal;
14
14
  maxDurationMs?: number;
15
15
  }) => Promise<number>;
16
+ __zintrustQueueWorkerMeta?: Readonly<{
17
+ kindLabel: string;
18
+ defaultQueueName: string;
19
+ maxAttempts: number;
20
+ }>;
16
21
  };
17
22
  export type CreateQueueWorkerOptions<TPayload> = {
18
23
  kindLabel: string;
@@ -235,7 +235,7 @@ const startTrackingAndHeartbeat = async (input) => {
235
235
  return { startedAtMs, heartbeatTimer };
236
236
  };
237
237
  const processQueueMessage = async (options, queueName, driverName) => {
238
- const message = await Queue.dequeue(queueName, driverName);
238
+ const message = (await Queue.dequeue(queueName, driverName));
239
239
  if (!message)
240
240
  return false;
241
241
  const baseLogFields = buildBaseLogFields(message, options.getLogFields);
@@ -363,5 +363,16 @@ export function createQueueWorker(options) {
363
363
  const processAll = createProcessAll(options.defaultQueueName, processOne);
364
364
  const runOnce = createRunOnce(options.defaultQueueName, processOne);
365
365
  const startWorker = createStartWorker(options.kindLabel, options.defaultQueueName, processOne);
366
- return Object.freeze({ processOne, processAll, runOnce, startWorker });
366
+ const queueWorkerMeta = Object.freeze({
367
+ kindLabel: options.kindLabel,
368
+ defaultQueueName: options.defaultQueueName,
369
+ maxAttempts: options.maxAttempts,
370
+ });
371
+ return Object.freeze({
372
+ processOne,
373
+ processAll,
374
+ runOnce,
375
+ startWorker,
376
+ __zintrustQueueWorkerMeta: queueWorkerMeta,
377
+ });
367
378
  }