@getworkbench/core 0.2.1 → 0.3.1

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.
@@ -0,0 +1,696 @@
1
+ import { Queue, RedisOptions } from 'bullmq';
2
+
3
+ /**
4
+ * Job status types matching BullMQ states
5
+ */
6
+ type JobStatus = "active" | "waiting" | "waiting-children" | "prioritized" | "completed" | "failed" | "delayed" | "paused" | "unknown";
7
+ /**
8
+ * Configuration options for Workbench
9
+ */
10
+ interface WorkbenchOptions {
11
+ /** BullMQ Queue instances to display */
12
+ queues?: Queue[];
13
+ /** Redis connection for auto-discovery of queues */
14
+ redis?: string | RedisOptions;
15
+ /** Basic auth credentials */
16
+ auth?: {
17
+ username: string;
18
+ password: string;
19
+ };
20
+ /** Dashboard title */
21
+ title?: string;
22
+ /** Logo URL */
23
+ logo?: string;
24
+ /** Override base path detection */
25
+ basePath?: string;
26
+ /** Disable actions (retry, remove, promote) */
27
+ readonly?: boolean;
28
+ /** Fields from job.data to extract as filterable tags (e.g., ['teamId', 'userId']) */
29
+ tags?: string[];
30
+ /**
31
+ * BullMQ key prefix used during queue auto-discovery from `redis`. Ignored
32
+ * when `queues` is set explicitly. Defaults to `"bull"`.
33
+ */
34
+ prefix?: string;
35
+ /**
36
+ * Maximum number of queues to keep when auto-discovering from `redis`.
37
+ * Prevents connection storms on very large Redis deployments. Defaults to
38
+ * 100. Ignored when `queues` is set explicitly.
39
+ */
40
+ maxQueues?: number;
41
+ }
42
+ /**
43
+ * Queue information for API responses
44
+ */
45
+ interface QueueInfo {
46
+ name: string;
47
+ counts: {
48
+ waiting: number;
49
+ active: number;
50
+ completed: number;
51
+ failed: number;
52
+ delayed: number;
53
+ prioritized: number;
54
+ "waiting-children": number;
55
+ paused: number;
56
+ };
57
+ isPaused: boolean;
58
+ }
59
+ /**
60
+ * Worker information from BullMQ
61
+ */
62
+ interface WorkerInfo {
63
+ id: string;
64
+ name: string;
65
+ addr: string;
66
+ age: number;
67
+ idle: number;
68
+ started: number;
69
+ queueName: string;
70
+ }
71
+ /**
72
+ * Extracted tag key-value pairs from job data
73
+ */
74
+ type JobTags = Record<string, string | number | boolean | null>;
75
+ /**
76
+ * Job information for API responses
77
+ */
78
+ interface JobInfo {
79
+ id: string;
80
+ name: string;
81
+ data: unknown;
82
+ opts: {
83
+ attempts?: number;
84
+ delay?: number;
85
+ priority?: number;
86
+ };
87
+ progress: number | object;
88
+ attemptsMade: number;
89
+ processedOn?: number;
90
+ finishedOn?: number;
91
+ timestamp: number;
92
+ failedReason?: string;
93
+ stacktrace?: string[];
94
+ returnvalue?: unknown;
95
+ status: JobStatus;
96
+ duration?: number;
97
+ /** Extracted tag values from job.data based on configured tag fields */
98
+ tags?: JobTags;
99
+ /** Parent job info if this job is part of a flow */
100
+ parent?: {
101
+ id: string;
102
+ queueName: string;
103
+ };
104
+ }
105
+ /**
106
+ * Overview stats for dashboard
107
+ */
108
+ interface OverviewStats {
109
+ totalJobs: number;
110
+ activeJobs: number;
111
+ failedJobs: number;
112
+ completedToday: number;
113
+ avgDuration: number;
114
+ queues: QueueInfo[];
115
+ }
116
+ /**
117
+ * Paginated response wrapper
118
+ */
119
+ interface PaginatedResponse<T> {
120
+ data: T[];
121
+ total: number;
122
+ cursor?: string;
123
+ hasMore: boolean;
124
+ }
125
+ /**
126
+ * Search result item
127
+ */
128
+ interface SearchResult {
129
+ queue: string;
130
+ job: JobInfo;
131
+ }
132
+ /**
133
+ * Run item - job execution with queue context
134
+ */
135
+ interface RunInfo extends JobInfo {
136
+ queueName: string;
137
+ }
138
+ /**
139
+ * Lightweight run info for list view - only fields needed for table display
140
+ * Excludes large fields like full job.data, opts, progress, etc.
141
+ */
142
+ interface RunInfoList {
143
+ id: string;
144
+ name: string;
145
+ status: JobStatus;
146
+ queueName: string;
147
+ tags?: JobTags;
148
+ processedOn?: number;
149
+ timestamp: number;
150
+ duration?: number;
151
+ }
152
+ /**
153
+ * Scheduler info for repeatable jobs
154
+ */
155
+ interface SchedulerInfo {
156
+ key: string;
157
+ name: string;
158
+ queueName: string;
159
+ pattern?: string;
160
+ every?: number;
161
+ next?: number;
162
+ endDate?: number;
163
+ tz?: string;
164
+ }
165
+ /**
166
+ * Delayed job info
167
+ */
168
+ interface DelayedJobInfo {
169
+ id: string;
170
+ name: string;
171
+ queueName: string;
172
+ delay: number;
173
+ processAt: number;
174
+ data: unknown;
175
+ }
176
+ /**
177
+ * Test job request
178
+ */
179
+ interface TestJobRequest {
180
+ queueName: string;
181
+ jobName: string;
182
+ data: unknown;
183
+ opts?: {
184
+ delay?: number;
185
+ priority?: number;
186
+ attempts?: number;
187
+ };
188
+ }
189
+ /**
190
+ * Sort direction
191
+ */
192
+ type SortDirection = "asc" | "desc";
193
+ /**
194
+ * Sort options for API requests
195
+ */
196
+ interface SortOptions {
197
+ field: string;
198
+ direction: SortDirection;
199
+ }
200
+ /**
201
+ * Valid sort fields for runs/jobs
202
+ */
203
+ type RunSortField = "timestamp" | "name" | "status" | "duration" | "queueName";
204
+ /**
205
+ * Valid sort fields for repeatable schedulers
206
+ */
207
+ type RepeatableSortField = "name" | "queueName" | "pattern" | "next" | "tz";
208
+ /**
209
+ * Valid sort fields for delayed schedulers
210
+ */
211
+ type DelayedSortField = "name" | "queueName" | "processAt" | "delay";
212
+ /**
213
+ * Hourly bucket for metrics aggregation
214
+ */
215
+ interface HourlyBucket {
216
+ /** Unix timestamp (start of hour) */
217
+ hour: number;
218
+ /** Number of completed jobs */
219
+ completed: number;
220
+ /** Number of failed jobs */
221
+ failed: number;
222
+ /** Average processing duration in ms */
223
+ avgDuration: number;
224
+ /** Average queue wait time in ms */
225
+ avgWaitTime: number;
226
+ }
227
+ /**
228
+ * Metrics for a single queue
229
+ */
230
+ interface QueueMetrics {
231
+ queueName: string;
232
+ buckets: HourlyBucket[];
233
+ summary: {
234
+ totalCompleted: number;
235
+ totalFailed: number;
236
+ /** Error rate as 0-1 */
237
+ errorRate: number;
238
+ /** Average processing duration in ms */
239
+ avgDuration: number;
240
+ /** Average queue wait time in ms */
241
+ avgWaitTime: number;
242
+ /** Average throughput per hour */
243
+ throughputPerHour: number;
244
+ };
245
+ }
246
+ /**
247
+ * Slowest job entry
248
+ */
249
+ interface SlowestJob {
250
+ name: string;
251
+ queueName: string;
252
+ duration: number;
253
+ jobId: string;
254
+ }
255
+ /**
256
+ * Most failing job type entry
257
+ */
258
+ interface FailingJobType {
259
+ name: string;
260
+ queueName: string;
261
+ failCount: number;
262
+ totalCount: number;
263
+ errorRate: number;
264
+ }
265
+ /**
266
+ * Complete metrics response
267
+ */
268
+ interface MetricsResponse {
269
+ /** Metrics per queue */
270
+ queues: QueueMetrics[];
271
+ /** Aggregated metrics across all queues */
272
+ aggregate: Omit<QueueMetrics, "queueName"> & {
273
+ queueName: "all";
274
+ };
275
+ /** Top 10 slowest jobs */
276
+ slowestJobs: SlowestJob[];
277
+ /** Top 10 most failing job types */
278
+ mostFailingTypes: FailingJobType[];
279
+ /** Timestamp when metrics were computed */
280
+ computedAt: number;
281
+ }
282
+ /**
283
+ * A node in a flow tree representing a job and its children
284
+ */
285
+ interface FlowNode {
286
+ job: JobInfo;
287
+ queueName: string;
288
+ children?: FlowNode[];
289
+ }
290
+ /**
291
+ * Flow summary for list view
292
+ */
293
+ interface FlowSummary {
294
+ /** Root job ID */
295
+ id: string;
296
+ /** Root job name */
297
+ name: string;
298
+ /** Queue containing root job */
299
+ queueName: string;
300
+ /** Root job status */
301
+ status: JobStatus;
302
+ /** Total number of jobs in flow */
303
+ totalJobs: number;
304
+ /** Number of completed jobs */
305
+ completedJobs: number;
306
+ /** Number of failed jobs */
307
+ failedJobs: number;
308
+ /** When flow was created */
309
+ timestamp: number;
310
+ /** Duration if completed */
311
+ duration?: number;
312
+ }
313
+ /**
314
+ * Request to create a test flow
315
+ */
316
+ interface CreateFlowRequest {
317
+ name: string;
318
+ queueName: string;
319
+ data?: unknown;
320
+ children: CreateFlowChildRequest[];
321
+ }
322
+ /**
323
+ * Child job in a flow creation request
324
+ */
325
+ interface CreateFlowChildRequest {
326
+ name: string;
327
+ queueName: string;
328
+ data?: unknown;
329
+ children?: CreateFlowChildRequest[];
330
+ }
331
+ /**
332
+ * Activity bucket for timeline
333
+ */
334
+ interface ActivityBucket {
335
+ /** Unix timestamp (start of bucket) */
336
+ time: number;
337
+ /** Number of completed jobs */
338
+ completed: number;
339
+ /** Number of failed jobs */
340
+ failed: number;
341
+ }
342
+ /**
343
+ * Activity stats response for the 7-day timeline
344
+ */
345
+ interface ActivityStatsResponse {
346
+ /** Activity buckets (4-hour intervals over 7 days) */
347
+ buckets: ActivityBucket[];
348
+ /** Start time of the first bucket */
349
+ startTime: number;
350
+ /** End time (now) */
351
+ endTime: number;
352
+ /** Size of each bucket in ms */
353
+ bucketSize: number;
354
+ /** Total completed in period */
355
+ totalCompleted: number;
356
+ /** Total failed in period */
357
+ totalFailed: number;
358
+ /** Timestamp when stats were computed */
359
+ computedAt: number;
360
+ }
361
+
362
+ /**
363
+ * Manages queue operations for the Workbench dashboard
364
+ */
365
+ declare class QueueManager {
366
+ private queues;
367
+ private tagFields;
368
+ private flowProducer;
369
+ private cache;
370
+ private readonly CACHE_TTL;
371
+ constructor(queues: Queue[], tagFields?: string[]);
372
+ /**
373
+ * Get cached value or compute and cache
374
+ */
375
+ private cached;
376
+ /**
377
+ * Execute a promise with a timeout
378
+ */
379
+ private withTimeout;
380
+ /**
381
+ * Get jobs by time range using Redis sorted sets (ZRANGEBYSCORE)
382
+ * This is more efficient than fetching all jobs and filtering in memory
383
+ */
384
+ private getJobsByTimeRange;
385
+ /**
386
+ * Cache for job state lookups to avoid repeated Redis calls
387
+ */
388
+ private jobStateCache;
389
+ /**
390
+ * Cache for job counts to avoid repeated Redis calls
391
+ * Short TTL since counts change frequently but are expensive to fetch
392
+ */
393
+ private countCache;
394
+ /**
395
+ * Get job counts with caching
396
+ */
397
+ private getCachedJobCounts;
398
+ /**
399
+ * Invalidate caches related to a job or queue
400
+ */
401
+ private invalidateJobCache;
402
+ /**
403
+ * Clear cache (useful after mutations)
404
+ */
405
+ clearCache(prefix?: string): void;
406
+ /**
407
+ * Get quick job counts across all queues (lightweight, for smart polling)
408
+ * Returns total counts per status - cached and very fast
409
+ */
410
+ getQuickCounts(): Promise<{
411
+ waiting: number;
412
+ active: number;
413
+ completed: number;
414
+ failed: number;
415
+ delayed: number;
416
+ prioritized: number;
417
+ "waiting-children": number;
418
+ total: number;
419
+ timestamp: number;
420
+ }>;
421
+ /**
422
+ * Get configured tag field names
423
+ */
424
+ getTagFields(): string[];
425
+ /**
426
+ * Get just queue names (very fast, no Redis calls)
427
+ * Used for sidebar initial render
428
+ */
429
+ getQueueNames(): string[];
430
+ /**
431
+ * Get a queue by name
432
+ */
433
+ getQueue(name: string): Queue | undefined;
434
+ /**
435
+ * Get information for all queues (cached)
436
+ */
437
+ getQueues(): Promise<QueueInfo[]>;
438
+ /**
439
+ * Get overview statistics (cached)
440
+ */
441
+ getOverview(): Promise<OverviewStats>;
442
+ /**
443
+ * Pause a queue - stops processing new jobs
444
+ */
445
+ pauseQueue(queueName: string): Promise<void>;
446
+ /**
447
+ * Resume a paused queue
448
+ */
449
+ resumeQueue(queueName: string): Promise<void>;
450
+ /**
451
+ * Check if a queue is paused
452
+ */
453
+ isQueuePaused(queueName: string): Promise<boolean>;
454
+ /**
455
+ * Get metrics for the last 24 hours (cached - expensive operation)
456
+ */
457
+ getMetrics(): Promise<MetricsResponse>;
458
+ /**
459
+ * Get activity stats for the last 7 days (cached)
460
+ * Returns 4-hour buckets for the activity timeline
461
+ */
462
+ getActivityStats(): Promise<ActivityStatsResponse>;
463
+ /**
464
+ * Get jobs for a specific queue with pagination and sorting
465
+ */
466
+ getJobs(queueName: string, status?: JobStatus, limit?: number, start?: number, sort?: SortOptions): Promise<PaginatedResponse<JobInfo>>;
467
+ /**
468
+ * Get a single job by ID
469
+ */
470
+ getJob(queueName: string, jobId: string): Promise<JobInfo | null>;
471
+ /**
472
+ * Retry a failed job
473
+ */
474
+ retryJob(queueName: string, jobId: string): Promise<boolean>;
475
+ /**
476
+ * Remove a job
477
+ */
478
+ removeJob(queueName: string, jobId: string): Promise<boolean>;
479
+ /**
480
+ * Promote a delayed job to waiting
481
+ */
482
+ promoteJob(queueName: string, jobId: string): Promise<boolean>;
483
+ /**
484
+ * Parse search query for field:value filters
485
+ * Returns { filters: { field: value }, text: remainingText }
486
+ */
487
+ private parseSearchQuery;
488
+ /**
489
+ * Check if a raw job matches all provided filters (before conversion)
490
+ * This is more efficient than converting to JobInfo first
491
+ */
492
+ private jobMatchesAllFilters;
493
+ /**
494
+ * Check if a job matches the given tag filters
495
+ */
496
+ private jobMatchesFilters;
497
+ /**
498
+ * Search jobs across all queues
499
+ * Supports field:value syntax (e.g., "teamId:abc-123 invoice")
500
+ * Optimized with parallel processing, early exits, and count checks
501
+ */
502
+ search(query: string, limit?: number): Promise<SearchResult[]>;
503
+ /**
504
+ * Clean jobs from a queue
505
+ */
506
+ cleanJobs(queueName: string, status: "completed" | "failed", grace?: number): Promise<number>;
507
+ /**
508
+ * FAST PATH: Get latest runs without filters
509
+ * Optimized for the common case of viewing newest jobs (timestamp desc, no filters)
510
+ * - Single getJobs call per queue (not per status type)
511
+ * - No count checks needed
512
+ * - Minimal Redis round-trips
513
+ */
514
+ private getLatestRuns;
515
+ /**
516
+ * Get all runs (jobs) across all queues with sorting and filtering
517
+ * Uses fast path for common case (no filters, timestamp desc)
518
+ */
519
+ getAllRuns(limit?: number, start?: number, sort?: SortOptions, filters?: {
520
+ status?: JobStatus;
521
+ tags?: Record<string, string>;
522
+ text?: string;
523
+ timeRange?: {
524
+ start: number;
525
+ end: number;
526
+ };
527
+ }): Promise<PaginatedResponse<RunInfoList>>;
528
+ /**
529
+ * Get all schedulers (repeatable and delayed jobs) with sorting
530
+ */
531
+ getSchedulers(repeatableSort?: SortOptions, delayedSort?: SortOptions): Promise<{
532
+ repeatable: SchedulerInfo[];
533
+ delayed: DelayedJobInfo[];
534
+ }>;
535
+ /**
536
+ * Enqueue a new job (for testing)
537
+ */
538
+ enqueueJob(request: TestJobRequest): Promise<{
539
+ id: string;
540
+ }>;
541
+ /**
542
+ * Extract tag values from job data based on configured tag fields
543
+ */
544
+ private extractTags;
545
+ /**
546
+ * Get unique values for a specific tag field across all jobs
547
+ */
548
+ getTagValues(field: string, limit?: number): Promise<{
549
+ value: string;
550
+ count: number;
551
+ }[]>;
552
+ /**
553
+ * Get sortable value from JobInfo/RunInfo
554
+ */
555
+ private getSortValue;
556
+ /**
557
+ * Get sortable value from RunInfoList (lightweight version)
558
+ */
559
+ private getSortValueForList;
560
+ /**
561
+ * Get sortable value from SchedulerInfo
562
+ */
563
+ private getSchedulerSortValue;
564
+ /**
565
+ * Get sortable value from DelayedJobInfo
566
+ */
567
+ private getDelayedSortValue;
568
+ /**
569
+ * Convert a BullMQ Job to JobInfo or RunInfoList
570
+ * @param job - The BullMQ job to convert
571
+ * @param fields - "list" for lightweight list view, "full" for complete job details
572
+ * @param knownState - Optional: skip getState() call if state is already known from fetch
573
+ */
574
+ private jobToInfo;
575
+ /**
576
+ * Retry multiple jobs across queues
577
+ * Processed in parallel for better performance
578
+ */
579
+ bulkRetry(jobs: {
580
+ queueName: string;
581
+ jobId: string;
582
+ }[]): Promise<{
583
+ success: number;
584
+ failed: number;
585
+ }>;
586
+ /**
587
+ * Delete multiple jobs across queues
588
+ * Processed in parallel for better performance
589
+ */
590
+ bulkDelete(jobs: {
591
+ queueName: string;
592
+ jobId: string;
593
+ }[]): Promise<{
594
+ success: number;
595
+ failed: number;
596
+ }>;
597
+ /**
598
+ * Promote multiple delayed jobs across queues (move to waiting)
599
+ * Processed in parallel for better performance
600
+ */
601
+ bulkPromote(jobs: {
602
+ queueName: string;
603
+ jobId: string;
604
+ }[]): Promise<{
605
+ success: number;
606
+ failed: number;
607
+ }>;
608
+ /**
609
+ * Get all flows (jobs that have children or are part of a flow) - cached
610
+ * Optimized to focus on waiting-children type first and early exit
611
+ */
612
+ getFlows(limit?: number): Promise<FlowSummary[]>;
613
+ /**
614
+ * Get a single flow tree by root job ID
615
+ */
616
+ getFlow(queueName: string, jobId: string): Promise<FlowNode | null>;
617
+ /**
618
+ * Create a new flow
619
+ */
620
+ createFlow(request: CreateFlowRequest): Promise<{
621
+ id: string;
622
+ }>;
623
+ /**
624
+ * Build a FlowJob from CreateFlowRequest or CreateFlowChildRequest
625
+ */
626
+ private buildFlowJob;
627
+ /**
628
+ * Convert BullMQ flow tree to our FlowNode structure
629
+ */
630
+ private convertFlowTree;
631
+ /**
632
+ * Count statistics for a flow tree
633
+ */
634
+ private countFlowStats;
635
+ }
636
+
637
+ /**
638
+ * Internal metadata produced by {@link WorkbenchCore.fromOptions} when queues
639
+ * are auto-discovered from a Redis connection.
640
+ */
641
+ interface DiscoveryMeta {
642
+ /** Total number of queues found on the connection (before capping). */
643
+ total: number;
644
+ /** True if the result was capped at `maxQueues`. */
645
+ capped: boolean;
646
+ /** The cap that was applied. */
647
+ cap: number;
648
+ }
649
+ /**
650
+ * Core Workbench class that manages the dashboard
651
+ */
652
+ declare class WorkbenchCore {
653
+ readonly options: Required<Pick<WorkbenchOptions, "title" | "readonly">> & WorkbenchOptions;
654
+ readonly queueManager: QueueManager;
655
+ readonly discovery: DiscoveryMeta | null;
656
+ constructor(options: WorkbenchOptions | Queue[], discovery?: DiscoveryMeta | null);
657
+ /**
658
+ * Async factory: build a `WorkbenchCore` from `WorkbenchOptions`, performing
659
+ * BullMQ queue auto-discovery via `SCAN <prefix>:*:meta` when `queues` is
660
+ * not provided.
661
+ *
662
+ * - When `queues` is set explicitly, behaves like `new WorkbenchCore(opts)`.
663
+ * - When only `redis` is set, scans the connection for queues, caps at
664
+ * `maxQueues` (default 100) to avoid connection storms with very large
665
+ * deployments, and constructs the core with the resulting list.
666
+ * - When no queues are discovered, the core is constructed with an empty
667
+ * queue map so the dashboard can render an "empty" state instead of
668
+ * erroring out.
669
+ */
670
+ static fromOptions(opts: WorkbenchOptions): Promise<WorkbenchCore>;
671
+ /**
672
+ * Get the queue manager instance
673
+ */
674
+ getQueueManager(): QueueManager;
675
+ /**
676
+ * Check if authentication is required
677
+ */
678
+ requiresAuth(): boolean;
679
+ /**
680
+ * Validate authentication credentials
681
+ */
682
+ validateAuth(username: string, password: string): boolean;
683
+ /**
684
+ * Get dashboard configuration for the UI
685
+ */
686
+ getConfig(): {
687
+ title: string;
688
+ logo: string | undefined;
689
+ readonly: boolean;
690
+ queues: string[];
691
+ tags: string[];
692
+ discovery: DiscoveryMeta | null;
693
+ };
694
+ }
695
+
696
+ export { type ActivityBucket as A, type CreateFlowChildRequest as C, type DelayedJobInfo as D, type FailingJobType as F, type HourlyBucket as H, type JobInfo as J, type MetricsResponse as M, type OverviewStats as O, type PaginatedResponse as P, type QueueInfo as Q, type RepeatableSortField as R, type SchedulerInfo as S, type TestJobRequest as T, WorkbenchCore as W, type ActivityStatsResponse as a, type CreateFlowRequest as b, type DelayedSortField as c, type DiscoveryMeta as d, type FlowNode as e, type FlowSummary as f, type JobStatus as g, type JobTags as h, QueueManager as i, type QueueMetrics as j, type RunInfo as k, type RunInfoList as l, type RunSortField as m, type SearchResult as n, type SlowestJob as o, type SortDirection as p, type SortOptions as q, type WorkbenchOptions as r, type WorkerInfo as s };