@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/README.md +26 -0
- package/dist/BroadcastWorker.d.ts +5 -0
- package/dist/NotificationWorker.d.ts +5 -0
- package/dist/WorkerFactory.d.ts +8 -0
- package/dist/WorkerFactory.js +305 -27
- package/dist/WorkerInit.d.ts +17 -0
- package/dist/WorkerInit.js +54 -2
- package/dist/WorkerShutdownDurableObject.d.ts +12 -0
- package/dist/WorkerShutdownDurableObject.js +41 -0
- package/dist/build-manifest.json +558 -0
- package/dist/createQueueWorker.d.ts +5 -0
- package/dist/createQueueWorker.js +13 -2
- package/dist/dashboard/workers-api.js +46 -8
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/register.d.ts +3 -3
- package/dist/register.js +10 -4
- package/dist/ui/workers/index.html +202 -0
- package/dist/ui/workers/main.js +1952 -0
- package/dist/ui/workers/styles.css +1350 -0
- package/dist/ui/workers/zintrust.svg +30 -0
- package/package.json +11 -4
- package/src/WorkerFactory.ts +439 -30
- package/src/WorkerInit.ts +81 -3
- package/src/createQueueWorker.ts +18 -2
- package/src/dashboard/workers-api.ts +60 -13
- package/src/index.ts +6 -1
- package/src/register.ts +13 -8
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
|
-
|
|
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
|
|
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);
|
package/src/createQueueWorker.ts
CHANGED
|
@@ -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
|
|
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({
|
|
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
|
|
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()
|
|
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 =
|
|
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 {
|
|
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
|
-
|
|
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
|
|
67
|
+
const coreCli = (await import('@zintrust/core/cli')) as unknown as {
|
|
63
68
|
OptionalCliCommandRegistry?: Registry;
|
|
64
69
|
};
|
|
65
70
|
|
|
66
|
-
if (
|
|
67
|
-
registerWorkerCliCommands(
|
|
71
|
+
if (coreCli.OptionalCliCommandRegistry !== undefined) {
|
|
72
|
+
registerWorkerCliCommands(coreCli.OptionalCliCommandRegistry);
|
|
68
73
|
}
|
|
69
74
|
} catch {
|
|
70
75
|
// no-op
|