servcraft 0.1.0 → 0.1.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.
- package/.claude/settings.local.json +29 -0
- package/.github/CODEOWNERS +18 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +46 -0
- package/.github/dependabot.yml +59 -0
- package/.github/workflows/ci.yml +188 -0
- package/.github/workflows/release.yml +195 -0
- package/AUDIT.md +602 -0
- package/README.md +1070 -1
- package/dist/cli/index.cjs +2026 -2168
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +2026 -2168
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +595 -616
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +114 -52
- package/dist/index.d.ts +114 -52
- package/dist/index.js +595 -616
- package/dist/index.js.map +1 -1
- package/docs/CLI-001_MULTI_DB_PLAN.md +546 -0
- package/docs/DATABASE_MULTI_ORM.md +399 -0
- package/docs/PHASE1_BREAKDOWN.md +346 -0
- package/docs/PROGRESS.md +550 -0
- package/docs/modules/ANALYTICS.md +226 -0
- package/docs/modules/API-VERSIONING.md +252 -0
- package/docs/modules/AUDIT.md +192 -0
- package/docs/modules/AUTH.md +431 -0
- package/docs/modules/CACHE.md +346 -0
- package/docs/modules/EMAIL.md +254 -0
- package/docs/modules/FEATURE-FLAG.md +291 -0
- package/docs/modules/I18N.md +294 -0
- package/docs/modules/MEDIA-PROCESSING.md +281 -0
- package/docs/modules/MFA.md +266 -0
- package/docs/modules/NOTIFICATION.md +311 -0
- package/docs/modules/OAUTH.md +237 -0
- package/docs/modules/PAYMENT.md +804 -0
- package/docs/modules/QUEUE.md +540 -0
- package/docs/modules/RATE-LIMIT.md +339 -0
- package/docs/modules/SEARCH.md +288 -0
- package/docs/modules/SECURITY.md +327 -0
- package/docs/modules/SESSION.md +382 -0
- package/docs/modules/SWAGGER.md +305 -0
- package/docs/modules/UPLOAD.md +296 -0
- package/docs/modules/USER.md +505 -0
- package/docs/modules/VALIDATION.md +294 -0
- package/docs/modules/WEBHOOK.md +270 -0
- package/docs/modules/WEBSOCKET.md +691 -0
- package/package.json +53 -38
- package/prisma/schema.prisma +395 -1
- package/src/cli/commands/add-module.ts +520 -87
- package/src/cli/commands/db.ts +3 -4
- package/src/cli/commands/docs.ts +256 -6
- package/src/cli/commands/generate.ts +12 -19
- package/src/cli/commands/init.ts +384 -214
- package/src/cli/index.ts +0 -4
- package/src/cli/templates/repository.ts +6 -1
- package/src/cli/templates/routes.ts +6 -21
- package/src/cli/utils/docs-generator.ts +6 -7
- package/src/cli/utils/env-manager.ts +717 -0
- package/src/cli/utils/field-parser.ts +16 -7
- package/src/cli/utils/interactive-prompt.ts +223 -0
- package/src/cli/utils/template-manager.ts +346 -0
- package/src/config/database.config.ts +183 -0
- package/src/config/env.ts +0 -10
- package/src/config/index.ts +0 -14
- package/src/core/server.ts +1 -1
- package/src/database/adapters/mongoose.adapter.ts +132 -0
- package/src/database/adapters/prisma.adapter.ts +118 -0
- package/src/database/connection.ts +190 -0
- package/src/database/interfaces/database.interface.ts +85 -0
- package/src/database/interfaces/index.ts +7 -0
- package/src/database/interfaces/repository.interface.ts +129 -0
- package/src/database/models/mongoose/index.ts +7 -0
- package/src/database/models/mongoose/payment.schema.ts +347 -0
- package/src/database/models/mongoose/user.schema.ts +154 -0
- package/src/database/prisma.ts +1 -4
- package/src/database/redis.ts +101 -0
- package/src/database/repositories/mongoose/index.ts +7 -0
- package/src/database/repositories/mongoose/payment.repository.ts +380 -0
- package/src/database/repositories/mongoose/user.repository.ts +255 -0
- package/src/database/seed.ts +6 -1
- package/src/index.ts +9 -20
- package/src/middleware/security.ts +2 -6
- package/src/modules/analytics/analytics.routes.ts +80 -0
- package/src/modules/analytics/analytics.service.ts +364 -0
- package/src/modules/analytics/index.ts +18 -0
- package/src/modules/analytics/types.ts +180 -0
- package/src/modules/api-versioning/index.ts +15 -0
- package/src/modules/api-versioning/types.ts +86 -0
- package/src/modules/api-versioning/versioning.middleware.ts +120 -0
- package/src/modules/api-versioning/versioning.routes.ts +54 -0
- package/src/modules/api-versioning/versioning.service.ts +189 -0
- package/src/modules/audit/audit.repository.ts +206 -0
- package/src/modules/audit/audit.service.ts +27 -59
- package/src/modules/auth/auth.controller.ts +2 -2
- package/src/modules/auth/auth.middleware.ts +3 -9
- package/src/modules/auth/auth.routes.ts +10 -107
- package/src/modules/auth/auth.service.ts +126 -23
- package/src/modules/auth/index.ts +3 -4
- package/src/modules/cache/cache.service.ts +367 -0
- package/src/modules/cache/index.ts +10 -0
- package/src/modules/cache/types.ts +44 -0
- package/src/modules/email/email.service.ts +3 -10
- package/src/modules/email/templates.ts +2 -8
- package/src/modules/feature-flag/feature-flag.repository.ts +303 -0
- package/src/modules/feature-flag/feature-flag.routes.ts +247 -0
- package/src/modules/feature-flag/feature-flag.service.ts +566 -0
- package/src/modules/feature-flag/index.ts +20 -0
- package/src/modules/feature-flag/types.ts +192 -0
- package/src/modules/i18n/i18n.middleware.ts +186 -0
- package/src/modules/i18n/i18n.routes.ts +191 -0
- package/src/modules/i18n/i18n.service.ts +456 -0
- package/src/modules/i18n/index.ts +18 -0
- package/src/modules/i18n/types.ts +118 -0
- package/src/modules/media-processing/index.ts +17 -0
- package/src/modules/media-processing/media-processing.routes.ts +111 -0
- package/src/modules/media-processing/media-processing.service.ts +245 -0
- package/src/modules/media-processing/types.ts +156 -0
- package/src/modules/mfa/index.ts +20 -0
- package/src/modules/mfa/mfa.repository.ts +206 -0
- package/src/modules/mfa/mfa.routes.ts +595 -0
- package/src/modules/mfa/mfa.service.ts +572 -0
- package/src/modules/mfa/totp.ts +150 -0
- package/src/modules/mfa/types.ts +57 -0
- package/src/modules/notification/index.ts +20 -0
- package/src/modules/notification/notification.repository.ts +356 -0
- package/src/modules/notification/notification.service.ts +483 -0
- package/src/modules/notification/types.ts +119 -0
- package/src/modules/oauth/index.ts +20 -0
- package/src/modules/oauth/oauth.repository.ts +219 -0
- package/src/modules/oauth/oauth.routes.ts +446 -0
- package/src/modules/oauth/oauth.service.ts +293 -0
- package/src/modules/oauth/providers/apple.provider.ts +250 -0
- package/src/modules/oauth/providers/facebook.provider.ts +181 -0
- package/src/modules/oauth/providers/github.provider.ts +248 -0
- package/src/modules/oauth/providers/google.provider.ts +189 -0
- package/src/modules/oauth/providers/twitter.provider.ts +214 -0
- package/src/modules/oauth/types.ts +94 -0
- package/src/modules/payment/index.ts +19 -0
- package/src/modules/payment/payment.repository.ts +733 -0
- package/src/modules/payment/payment.routes.ts +390 -0
- package/src/modules/payment/payment.service.ts +354 -0
- package/src/modules/payment/providers/mobile-money.provider.ts +274 -0
- package/src/modules/payment/providers/paypal.provider.ts +190 -0
- package/src/modules/payment/providers/stripe.provider.ts +215 -0
- package/src/modules/payment/types.ts +140 -0
- package/src/modules/queue/cron.ts +438 -0
- package/src/modules/queue/index.ts +87 -0
- package/src/modules/queue/queue.routes.ts +600 -0
- package/src/modules/queue/queue.service.ts +842 -0
- package/src/modules/queue/types.ts +222 -0
- package/src/modules/queue/workers.ts +366 -0
- package/src/modules/rate-limit/index.ts +59 -0
- package/src/modules/rate-limit/rate-limit.middleware.ts +134 -0
- package/src/modules/rate-limit/rate-limit.routes.ts +269 -0
- package/src/modules/rate-limit/rate-limit.service.ts +348 -0
- package/src/modules/rate-limit/stores/memory.store.ts +165 -0
- package/src/modules/rate-limit/stores/redis.store.ts +322 -0
- package/src/modules/rate-limit/types.ts +153 -0
- package/src/modules/search/adapters/elasticsearch.adapter.ts +326 -0
- package/src/modules/search/adapters/meilisearch.adapter.ts +261 -0
- package/src/modules/search/adapters/memory.adapter.ts +278 -0
- package/src/modules/search/index.ts +21 -0
- package/src/modules/search/search.service.ts +234 -0
- package/src/modules/search/types.ts +214 -0
- package/src/modules/security/index.ts +40 -0
- package/src/modules/security/sanitize.ts +223 -0
- package/src/modules/security/security-audit.service.ts +388 -0
- package/src/modules/security/security.middleware.ts +398 -0
- package/src/modules/session/index.ts +3 -0
- package/src/modules/session/session.repository.ts +159 -0
- package/src/modules/session/session.service.ts +340 -0
- package/src/modules/session/types.ts +38 -0
- package/src/modules/swagger/index.ts +7 -1
- package/src/modules/swagger/schema-builder.ts +16 -4
- package/src/modules/swagger/swagger.service.ts +9 -10
- package/src/modules/swagger/types.ts +0 -2
- package/src/modules/upload/index.ts +14 -0
- package/src/modules/upload/types.ts +83 -0
- package/src/modules/upload/upload.repository.ts +199 -0
- package/src/modules/upload/upload.routes.ts +311 -0
- package/src/modules/upload/upload.service.ts +448 -0
- package/src/modules/user/index.ts +3 -3
- package/src/modules/user/user.controller.ts +15 -9
- package/src/modules/user/user.repository.ts +237 -113
- package/src/modules/user/user.routes.ts +39 -164
- package/src/modules/user/user.service.ts +4 -3
- package/src/modules/validation/validator.ts +12 -17
- package/src/modules/webhook/index.ts +91 -0
- package/src/modules/webhook/retry.ts +196 -0
- package/src/modules/webhook/signature.ts +135 -0
- package/src/modules/webhook/types.ts +181 -0
- package/src/modules/webhook/webhook.repository.ts +358 -0
- package/src/modules/webhook/webhook.routes.ts +442 -0
- package/src/modules/webhook/webhook.service.ts +457 -0
- package/src/modules/websocket/features.ts +504 -0
- package/src/modules/websocket/index.ts +106 -0
- package/src/modules/websocket/middlewares.ts +298 -0
- package/src/modules/websocket/types.ts +181 -0
- package/src/modules/websocket/websocket.service.ts +692 -0
- package/src/utils/errors.ts +7 -0
- package/src/utils/pagination.ts +4 -1
- package/tests/helpers/db-check.ts +79 -0
- package/tests/integration/auth-redis.test.ts +94 -0
- package/tests/integration/cache-redis.test.ts +387 -0
- package/tests/integration/mongoose-repositories.test.ts +410 -0
- package/tests/integration/payment-prisma.test.ts +637 -0
- package/tests/integration/queue-bullmq.test.ts +417 -0
- package/tests/integration/user-prisma.test.ts +441 -0
- package/tests/integration/websocket-socketio.test.ts +552 -0
- package/tests/setup.ts +11 -9
- package/vitest.config.ts +3 -8
- package/npm-cache/_cacache/content-v2/sha512/1c/d0/03440d500a0487621aad1d6402978340698976602046db8e24fa03c01ee6c022c69b0582f969042d9442ee876ac35c038e960dd427d1e622fa24b8eb7dba +0 -0
- package/npm-cache/_cacache/content-v2/sha512/42/55/28b493ca491833e5aab0e9c3108d29ab3f36c248ca88f45d4630674fce9130959e56ae308797ac2b6328fa7f09a610b9550ed09cb971d039876d293fc69d +0 -0
- package/npm-cache/_cacache/content-v2/sha512/e0/12/f360dc9315ee5f17844a0c8c233ee6bf7c30837c4a02ea0d56c61c7f7ab21c0e958e50ed2c57c59f983c762b93056778c9009b2398ffc26def0183999b13 +0 -0
- package/npm-cache/_cacache/content-v2/sha512/ed/b0/fae1161902898f4c913c67d7f6cdf6be0665aec3b389b9c4f4f0a101ca1da59badf1b59c4e0030f5223023b8d63cfe501c46a32c20c895d4fb3f11ca2232 +0 -0
- package/npm-cache/_cacache/index-v5/58/94/c2cba79e0f16b4c10e95a87e32255741149e8222cc314a476aab67c39cc0 +0 -5
|
@@ -0,0 +1,842 @@
|
|
|
1
|
+
import type { Job as BullJob } from 'bullmq';
|
|
2
|
+
import { Queue, Worker as BullWorker, QueueEvents } from 'bullmq';
|
|
3
|
+
import { EventEmitter } from 'events';
|
|
4
|
+
import { Redis } from 'ioredis';
|
|
5
|
+
import { logger } from '../../core/logger.js';
|
|
6
|
+
import { NotFoundError, BadRequestError } from '../../utils/errors.js';
|
|
7
|
+
import type {
|
|
8
|
+
Job,
|
|
9
|
+
JobOptions,
|
|
10
|
+
QueueConfig,
|
|
11
|
+
QueueStats,
|
|
12
|
+
Worker,
|
|
13
|
+
JobStatus,
|
|
14
|
+
JobFilter,
|
|
15
|
+
BulkJobOptions,
|
|
16
|
+
QueueMetrics,
|
|
17
|
+
JobEvent,
|
|
18
|
+
} from './types.js';
|
|
19
|
+
|
|
20
|
+
const defaultConfig: Required<QueueConfig> = {
|
|
21
|
+
redis: {
|
|
22
|
+
host: process.env.REDIS_HOST || 'localhost',
|
|
23
|
+
port: parseInt(process.env.REDIS_PORT || '6379', 10),
|
|
24
|
+
password: process.env.REDIS_PASSWORD,
|
|
25
|
+
db: parseInt(process.env.REDIS_DB || '0', 10),
|
|
26
|
+
},
|
|
27
|
+
defaultJobOptions: {
|
|
28
|
+
attempts: 3,
|
|
29
|
+
backoff: {
|
|
30
|
+
type: 'exponential',
|
|
31
|
+
delay: 1000,
|
|
32
|
+
},
|
|
33
|
+
removeOnComplete: true,
|
|
34
|
+
removeOnFail: false,
|
|
35
|
+
timeout: 60000,
|
|
36
|
+
},
|
|
37
|
+
prefix: 'servcraft:queue',
|
|
38
|
+
metrics: true,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Queue Service
|
|
43
|
+
* Manages background jobs and task queues using BullMQ with Redis persistence
|
|
44
|
+
*
|
|
45
|
+
* Features:
|
|
46
|
+
* - Persistent job storage in Redis
|
|
47
|
+
* - Automatic retries with exponential backoff
|
|
48
|
+
* - Job prioritization (critical, high, normal, low)
|
|
49
|
+
* - Delayed/scheduled jobs
|
|
50
|
+
* - Repeatable/cron jobs
|
|
51
|
+
* - Concurrency control per worker
|
|
52
|
+
* - Real-time events and metrics
|
|
53
|
+
* - Multi-instance safe (horizontal scaling)
|
|
54
|
+
*/
|
|
55
|
+
export class QueueService extends EventEmitter {
|
|
56
|
+
private config: Required<QueueConfig>;
|
|
57
|
+
private connection: Redis;
|
|
58
|
+
private queues = new Map<string, Queue>();
|
|
59
|
+
private workers = new Map<string, BullWorker>();
|
|
60
|
+
private queueEvents = new Map<string, QueueEvents>();
|
|
61
|
+
private workerProcessors = new Map<string, Map<string, Worker>>();
|
|
62
|
+
private metrics = new Map<string, QueueMetrics>();
|
|
63
|
+
private isClosing = false;
|
|
64
|
+
|
|
65
|
+
constructor(config?: QueueConfig) {
|
|
66
|
+
super();
|
|
67
|
+
this.config = {
|
|
68
|
+
...defaultConfig,
|
|
69
|
+
...config,
|
|
70
|
+
redis: { ...defaultConfig.redis, ...config?.redis },
|
|
71
|
+
defaultJobOptions: { ...defaultConfig.defaultJobOptions, ...config?.defaultJobOptions },
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// Create Redis connection
|
|
75
|
+
this.connection = new Redis({
|
|
76
|
+
host: this.config.redis.host,
|
|
77
|
+
port: this.config.redis.port,
|
|
78
|
+
password: this.config.redis.password,
|
|
79
|
+
db: this.config.redis.db,
|
|
80
|
+
maxRetriesPerRequest: null, // Required for BullMQ
|
|
81
|
+
enableReadyCheck: false,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
this.connection.on('error', (err: Error) => {
|
|
85
|
+
logger.error({ err }, 'Queue Redis connection error');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
this.connection.on('connect', () => {
|
|
89
|
+
logger.info('Queue Redis connected');
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Create a new queue
|
|
95
|
+
*/
|
|
96
|
+
createQueue(name: string): Queue {
|
|
97
|
+
if (this.queues.has(name)) {
|
|
98
|
+
return this.queues.get(name)!;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const queue = new Queue(name, {
|
|
102
|
+
connection: this.connection.duplicate(),
|
|
103
|
+
prefix: this.config.prefix,
|
|
104
|
+
defaultJobOptions: this.mapJobOptionsToBullMQ(this.config.defaultJobOptions),
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
this.queues.set(name, queue);
|
|
108
|
+
this.workerProcessors.set(name, new Map());
|
|
109
|
+
|
|
110
|
+
// Initialize metrics
|
|
111
|
+
if (this.config.metrics) {
|
|
112
|
+
this.metrics.set(name, {
|
|
113
|
+
totalProcessed: 0,
|
|
114
|
+
totalFailed: 0,
|
|
115
|
+
avgProcessingTime: 0,
|
|
116
|
+
throughput: 0,
|
|
117
|
+
peakConcurrency: 0,
|
|
118
|
+
currentConcurrency: 0,
|
|
119
|
+
successRate: 100,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Setup queue events
|
|
124
|
+
this.setupQueueEvents(name);
|
|
125
|
+
|
|
126
|
+
logger.info({ queueName: name }, 'Queue created with BullMQ');
|
|
127
|
+
|
|
128
|
+
return queue;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Setup queue events listener
|
|
133
|
+
*/
|
|
134
|
+
private setupQueueEvents(name: string): void {
|
|
135
|
+
const queueEvents = new QueueEvents(name, {
|
|
136
|
+
connection: this.connection.duplicate(),
|
|
137
|
+
prefix: this.config.prefix,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
this.queueEvents.set(name, queueEvents);
|
|
141
|
+
|
|
142
|
+
queueEvents.on('completed', ({ jobId, returnvalue }) => {
|
|
143
|
+
this.emitEvent({
|
|
144
|
+
event: 'completed',
|
|
145
|
+
jobId,
|
|
146
|
+
data: returnvalue,
|
|
147
|
+
timestamp: new Date(),
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
queueEvents.on('failed', ({ jobId, failedReason }) => {
|
|
152
|
+
this.emitEvent({
|
|
153
|
+
event: 'failed',
|
|
154
|
+
jobId,
|
|
155
|
+
data: failedReason,
|
|
156
|
+
timestamp: new Date(),
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
queueEvents.on('progress', ({ jobId, data }) => {
|
|
161
|
+
this.emitEvent({
|
|
162
|
+
event: 'progress',
|
|
163
|
+
jobId,
|
|
164
|
+
data,
|
|
165
|
+
timestamp: new Date(),
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
queueEvents.on('stalled', ({ jobId }) => {
|
|
170
|
+
this.emitEvent({
|
|
171
|
+
event: 'stalled',
|
|
172
|
+
jobId,
|
|
173
|
+
timestamp: new Date(),
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
queueEvents.on('active', ({ jobId }) => {
|
|
178
|
+
this.emitEvent({
|
|
179
|
+
event: 'active',
|
|
180
|
+
jobId,
|
|
181
|
+
timestamp: new Date(),
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Map our JobOptions to BullMQ options
|
|
188
|
+
*/
|
|
189
|
+
private mapJobOptionsToBullMQ(options: JobOptions): Record<string, unknown> {
|
|
190
|
+
const bullOptions: Record<string, unknown> = {};
|
|
191
|
+
|
|
192
|
+
if (options.priority) {
|
|
193
|
+
// BullMQ uses numeric priority (lower = higher priority)
|
|
194
|
+
const priorityMap = { critical: 1, high: 2, normal: 3, low: 4 };
|
|
195
|
+
bullOptions.priority = priorityMap[options.priority];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (options.delay !== undefined) {
|
|
199
|
+
bullOptions.delay = options.delay;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (options.attempts !== undefined) {
|
|
203
|
+
bullOptions.attempts = options.attempts;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (options.backoff) {
|
|
207
|
+
bullOptions.backoff = {
|
|
208
|
+
type: options.backoff.type,
|
|
209
|
+
delay: options.backoff.delay,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (options.removeOnComplete !== undefined) {
|
|
214
|
+
bullOptions.removeOnComplete = options.removeOnComplete;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (options.removeOnFail !== undefined) {
|
|
218
|
+
bullOptions.removeOnFail = options.removeOnFail;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (options.repeat) {
|
|
222
|
+
bullOptions.repeat = {
|
|
223
|
+
pattern: options.repeat.cron,
|
|
224
|
+
every: options.repeat.every,
|
|
225
|
+
limit: options.repeat.limit,
|
|
226
|
+
immediately: options.repeat.immediately,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return bullOptions;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Map BullMQ job to our Job interface
|
|
235
|
+
*/
|
|
236
|
+
private mapBullJobToJob<T>(bullJob: BullJob<T>, queueName: string): Job<T> {
|
|
237
|
+
const state = bullJob.returnvalue !== undefined ? 'completed' : 'waiting';
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
id: bullJob.id || '',
|
|
241
|
+
queueName,
|
|
242
|
+
name: bullJob.name,
|
|
243
|
+
data: bullJob.data,
|
|
244
|
+
options: {
|
|
245
|
+
priority: this.mapPriorityFromBullMQ(bullJob.opts.priority),
|
|
246
|
+
delay: bullJob.opts.delay,
|
|
247
|
+
attempts: bullJob.opts.attempts,
|
|
248
|
+
backoff: bullJob.opts.backoff as JobOptions['backoff'],
|
|
249
|
+
removeOnComplete: bullJob.opts.removeOnComplete as boolean | number | undefined,
|
|
250
|
+
removeOnFail: bullJob.opts.removeOnFail as boolean | number | undefined,
|
|
251
|
+
},
|
|
252
|
+
status: state as JobStatus,
|
|
253
|
+
progress: typeof bullJob.progress === 'number' ? bullJob.progress : undefined,
|
|
254
|
+
attemptsMade: bullJob.attemptsMade,
|
|
255
|
+
result: bullJob.returnvalue,
|
|
256
|
+
error: bullJob.failedReason,
|
|
257
|
+
stacktrace: bullJob.stacktrace,
|
|
258
|
+
createdAt: new Date(bullJob.timestamp),
|
|
259
|
+
processedAt: bullJob.processedOn ? new Date(bullJob.processedOn) : undefined,
|
|
260
|
+
completedAt: bullJob.finishedOn ? new Date(bullJob.finishedOn) : undefined,
|
|
261
|
+
failedAt: bullJob.failedReason ? new Date(bullJob.finishedOn || Date.now()) : undefined,
|
|
262
|
+
delayedUntil: bullJob.delay ? new Date(bullJob.timestamp + bullJob.delay) : undefined,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Map BullMQ numeric priority to our priority type
|
|
268
|
+
*/
|
|
269
|
+
private mapPriorityFromBullMQ(priority?: number): JobOptions['priority'] {
|
|
270
|
+
if (!priority) return 'normal';
|
|
271
|
+
if (priority <= 1) return 'critical';
|
|
272
|
+
if (priority <= 2) return 'high';
|
|
273
|
+
if (priority <= 3) return 'normal';
|
|
274
|
+
return 'low';
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Add a job to a queue
|
|
279
|
+
*/
|
|
280
|
+
async addJob<T = unknown>(
|
|
281
|
+
queueName: string,
|
|
282
|
+
jobName: string,
|
|
283
|
+
data: T,
|
|
284
|
+
options?: JobOptions
|
|
285
|
+
): Promise<Job<T>> {
|
|
286
|
+
const queue = this.createQueue(queueName);
|
|
287
|
+
|
|
288
|
+
const mergedOptions = { ...this.config.defaultJobOptions, ...options };
|
|
289
|
+
const bullOptions = this.mapJobOptionsToBullMQ(mergedOptions);
|
|
290
|
+
|
|
291
|
+
const bullJob = await queue.add(jobName, data, bullOptions);
|
|
292
|
+
|
|
293
|
+
const job = this.mapBullJobToJob<T>(bullJob, queueName);
|
|
294
|
+
|
|
295
|
+
this.emitEvent({
|
|
296
|
+
event: 'added',
|
|
297
|
+
jobId: job.id,
|
|
298
|
+
data: job,
|
|
299
|
+
timestamp: new Date(),
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
logger.debug({ jobId: job.id, queueName, jobName }, 'Job added to queue');
|
|
303
|
+
|
|
304
|
+
return job;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Add multiple jobs in bulk
|
|
309
|
+
*/
|
|
310
|
+
async addBulkJobs(queueName: string, bulkOptions: BulkJobOptions): Promise<Job[]> {
|
|
311
|
+
const queue = this.createQueue(queueName);
|
|
312
|
+
|
|
313
|
+
const bullJobs = bulkOptions.jobs.map((jobData) => ({
|
|
314
|
+
name: jobData.name,
|
|
315
|
+
data: jobData.data,
|
|
316
|
+
opts: jobData.opts ? this.mapJobOptionsToBullMQ(jobData.opts) : undefined,
|
|
317
|
+
}));
|
|
318
|
+
|
|
319
|
+
const addedJobs = await queue.addBulk(bullJobs);
|
|
320
|
+
|
|
321
|
+
const jobs = addedJobs.map((bullJob) => this.mapBullJobToJob(bullJob, queueName));
|
|
322
|
+
|
|
323
|
+
logger.info({ queueName, count: jobs.length }, 'Bulk jobs added');
|
|
324
|
+
|
|
325
|
+
return jobs;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Register a worker for a job type
|
|
330
|
+
*/
|
|
331
|
+
registerWorker<T = unknown>(queueName: string, worker: Worker<T>): void {
|
|
332
|
+
this.createQueue(queueName);
|
|
333
|
+
|
|
334
|
+
// Store the worker processor
|
|
335
|
+
this.workerProcessors.get(queueName)!.set(worker.name, worker as Worker);
|
|
336
|
+
|
|
337
|
+
// Create or update BullMQ worker
|
|
338
|
+
this.ensureBullWorker(queueName);
|
|
339
|
+
|
|
340
|
+
logger.info({ queueName, workerName: worker.name }, 'Worker registered');
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Ensure BullMQ worker exists for queue
|
|
345
|
+
*/
|
|
346
|
+
private ensureBullWorker(queueName: string): void {
|
|
347
|
+
// If worker already exists, close it and recreate
|
|
348
|
+
if (this.workers.has(queueName)) {
|
|
349
|
+
return; // Worker already running
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const processors = this.workerProcessors.get(queueName);
|
|
353
|
+
if (!processors || processors.size === 0) {
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Get max concurrency from all workers
|
|
358
|
+
const maxConcurrency = Math.max(
|
|
359
|
+
...Array.from(processors.values()).map((w) => w.concurrency || 1)
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
const bullWorker = new BullWorker(
|
|
363
|
+
queueName,
|
|
364
|
+
async (bullJob: BullJob) => {
|
|
365
|
+
const worker = processors.get(bullJob.name);
|
|
366
|
+
if (!worker) {
|
|
367
|
+
throw new Error(`No worker registered for job type: ${bullJob.name}`);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const job = this.mapBullJobToJob(bullJob, queueName);
|
|
371
|
+
const startTime = Date.now();
|
|
372
|
+
|
|
373
|
+
try {
|
|
374
|
+
const result = await worker.process(job);
|
|
375
|
+
|
|
376
|
+
// Update metrics on success
|
|
377
|
+
this.updateMetrics(queueName, true, Date.now() - startTime);
|
|
378
|
+
|
|
379
|
+
return result;
|
|
380
|
+
} catch (error) {
|
|
381
|
+
// Update metrics on failure
|
|
382
|
+
this.updateMetrics(queueName, false, Date.now() - startTime);
|
|
383
|
+
throw error;
|
|
384
|
+
}
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
connection: this.connection.duplicate(),
|
|
388
|
+
prefix: this.config.prefix,
|
|
389
|
+
concurrency: maxConcurrency,
|
|
390
|
+
}
|
|
391
|
+
);
|
|
392
|
+
|
|
393
|
+
bullWorker.on('completed', (job) => {
|
|
394
|
+
logger.info({ jobId: job.id, queueName, jobName: job.name }, 'Job completed');
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
bullWorker.on('failed', (job, err) => {
|
|
398
|
+
logger.error(
|
|
399
|
+
{ jobId: job?.id, queueName, jobName: job?.name, error: err.message },
|
|
400
|
+
'Job failed'
|
|
401
|
+
);
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
bullWorker.on('error', (err) => {
|
|
405
|
+
logger.error({ queueName, error: err.message }, 'Worker error');
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
this.workers.set(queueName, bullWorker);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Update metrics
|
|
413
|
+
*/
|
|
414
|
+
private updateMetrics(queueName: string, success: boolean, duration: number): void {
|
|
415
|
+
if (!this.config.metrics) return;
|
|
416
|
+
|
|
417
|
+
const metric = this.metrics.get(queueName);
|
|
418
|
+
if (!metric) return;
|
|
419
|
+
|
|
420
|
+
if (success) {
|
|
421
|
+
metric.totalProcessed++;
|
|
422
|
+
} else {
|
|
423
|
+
metric.totalFailed++;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Update average processing time
|
|
427
|
+
const total = metric.totalProcessed + metric.totalFailed;
|
|
428
|
+
metric.avgProcessingTime = (metric.avgProcessingTime * (total - 1) + duration) / total;
|
|
429
|
+
|
|
430
|
+
// Update success rate
|
|
431
|
+
metric.successRate = total > 0 ? (metric.totalProcessed / total) * 100 : 100;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Get job by ID
|
|
436
|
+
*/
|
|
437
|
+
async getJob(queueName: string, jobId: string): Promise<Job> {
|
|
438
|
+
const queue = this.queues.get(queueName);
|
|
439
|
+
if (!queue) {
|
|
440
|
+
throw new NotFoundError('Queue not found');
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const bullJob = await queue.getJob(jobId);
|
|
444
|
+
if (!bullJob) {
|
|
445
|
+
throw new NotFoundError('Job not found');
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Get actual state from BullMQ
|
|
449
|
+
const state = await bullJob.getState();
|
|
450
|
+
const job = this.mapBullJobToJob(bullJob, queueName);
|
|
451
|
+
job.status = state as JobStatus;
|
|
452
|
+
|
|
453
|
+
return job;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* List jobs with filters
|
|
458
|
+
*/
|
|
459
|
+
async listJobs(queueName: string, filter?: JobFilter): Promise<Job[]> {
|
|
460
|
+
const queue = this.queues.get(queueName);
|
|
461
|
+
if (!queue) {
|
|
462
|
+
throw new NotFoundError('Queue not found');
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const statuses = filter?.status
|
|
466
|
+
? Array.isArray(filter.status)
|
|
467
|
+
? filter.status
|
|
468
|
+
: [filter.status]
|
|
469
|
+
: ['waiting', 'active', 'completed', 'failed', 'delayed'];
|
|
470
|
+
|
|
471
|
+
const offset = filter?.offset || 0;
|
|
472
|
+
const limit = filter?.limit || 100;
|
|
473
|
+
|
|
474
|
+
// Get jobs from each status
|
|
475
|
+
const allJobs: Job[] = [];
|
|
476
|
+
|
|
477
|
+
for (const status of statuses) {
|
|
478
|
+
let bullJobs: BullJob[] = [];
|
|
479
|
+
|
|
480
|
+
switch (status) {
|
|
481
|
+
case 'waiting':
|
|
482
|
+
bullJobs = await queue.getWaiting(offset, offset + limit);
|
|
483
|
+
break;
|
|
484
|
+
case 'active':
|
|
485
|
+
bullJobs = await queue.getActive(offset, offset + limit);
|
|
486
|
+
break;
|
|
487
|
+
case 'completed':
|
|
488
|
+
bullJobs = await queue.getCompleted(offset, offset + limit);
|
|
489
|
+
break;
|
|
490
|
+
case 'failed':
|
|
491
|
+
bullJobs = await queue.getFailed(offset, offset + limit);
|
|
492
|
+
break;
|
|
493
|
+
case 'delayed':
|
|
494
|
+
bullJobs = await queue.getDelayed(offset, offset + limit);
|
|
495
|
+
break;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
for (const bullJob of bullJobs) {
|
|
499
|
+
const job = this.mapBullJobToJob(bullJob, queueName);
|
|
500
|
+
job.status = status as JobStatus;
|
|
501
|
+
|
|
502
|
+
// Apply name filter
|
|
503
|
+
if (filter?.name && job.name !== filter.name) {
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Apply date filters
|
|
508
|
+
if (filter?.startDate && job.createdAt < filter.startDate) {
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
if (filter?.endDate && job.createdAt > filter.endDate) {
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
allJobs.push(job);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Sort by creation date (newest first)
|
|
520
|
+
allJobs.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
|
521
|
+
|
|
522
|
+
// Apply pagination
|
|
523
|
+
return allJobs.slice(0, limit);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Get queue statistics
|
|
528
|
+
*/
|
|
529
|
+
async getStats(queueName: string): Promise<QueueStats> {
|
|
530
|
+
const queue = this.queues.get(queueName);
|
|
531
|
+
if (!queue) {
|
|
532
|
+
throw new NotFoundError('Queue not found');
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const counts = await queue.getJobCounts(
|
|
536
|
+
'waiting',
|
|
537
|
+
'active',
|
|
538
|
+
'completed',
|
|
539
|
+
'failed',
|
|
540
|
+
'delayed',
|
|
541
|
+
'paused'
|
|
542
|
+
);
|
|
543
|
+
|
|
544
|
+
return {
|
|
545
|
+
name: queueName,
|
|
546
|
+
waiting: counts.waiting || 0,
|
|
547
|
+
active: counts.active || 0,
|
|
548
|
+
completed: counts.completed || 0,
|
|
549
|
+
failed: counts.failed || 0,
|
|
550
|
+
delayed: counts.delayed || 0,
|
|
551
|
+
paused: counts.paused || 0,
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Get queue metrics
|
|
557
|
+
*/
|
|
558
|
+
async getMetrics(queueName: string): Promise<QueueMetrics> {
|
|
559
|
+
const metric = this.metrics.get(queueName);
|
|
560
|
+
if (!metric) {
|
|
561
|
+
throw new NotFoundError('Queue not found or metrics disabled');
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Update current concurrency from worker
|
|
565
|
+
const worker = this.workers.get(queueName);
|
|
566
|
+
if (worker) {
|
|
567
|
+
const queue = this.queues.get(queueName);
|
|
568
|
+
if (queue) {
|
|
569
|
+
const activeCount = await queue.getActiveCount();
|
|
570
|
+
metric.currentConcurrency = activeCount;
|
|
571
|
+
metric.peakConcurrency = Math.max(metric.peakConcurrency, activeCount);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
return { ...metric };
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Remove a job
|
|
580
|
+
*/
|
|
581
|
+
async removeJob(queueName: string, jobId: string): Promise<void> {
|
|
582
|
+
const queue = this.queues.get(queueName);
|
|
583
|
+
if (!queue) {
|
|
584
|
+
throw new NotFoundError('Queue not found');
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
const bullJob = await queue.getJob(jobId);
|
|
588
|
+
if (!bullJob) {
|
|
589
|
+
throw new NotFoundError('Job not found');
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const state = await bullJob.getState();
|
|
593
|
+
if (state === 'active') {
|
|
594
|
+
throw new BadRequestError('Cannot remove active job');
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
await bullJob.remove();
|
|
598
|
+
|
|
599
|
+
this.emitEvent({
|
|
600
|
+
event: 'removed',
|
|
601
|
+
jobId,
|
|
602
|
+
timestamp: new Date(),
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
logger.info({ jobId, queueName }, 'Job removed');
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Retry a failed job
|
|
610
|
+
*/
|
|
611
|
+
async retryJob(queueName: string, jobId: string): Promise<void> {
|
|
612
|
+
const queue = this.queues.get(queueName);
|
|
613
|
+
if (!queue) {
|
|
614
|
+
throw new NotFoundError('Queue not found');
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
const bullJob = await queue.getJob(jobId);
|
|
618
|
+
if (!bullJob) {
|
|
619
|
+
throw new NotFoundError('Job not found');
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
const state = await bullJob.getState();
|
|
623
|
+
if (state !== 'failed') {
|
|
624
|
+
throw new BadRequestError('Can only retry failed jobs');
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
await bullJob.retry();
|
|
628
|
+
|
|
629
|
+
logger.info({ jobId, queueName }, 'Job retry initiated');
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Clean completed/failed jobs
|
|
634
|
+
*/
|
|
635
|
+
async cleanJobs(
|
|
636
|
+
queueName: string,
|
|
637
|
+
status: JobStatus | JobStatus[],
|
|
638
|
+
olderThanMs: number = 24 * 60 * 60 * 1000
|
|
639
|
+
): Promise<number> {
|
|
640
|
+
const queue = this.queues.get(queueName);
|
|
641
|
+
if (!queue) {
|
|
642
|
+
throw new NotFoundError('Queue not found');
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
const statuses = Array.isArray(status) ? status : [status];
|
|
646
|
+
let cleaned = 0;
|
|
647
|
+
|
|
648
|
+
for (const s of statuses) {
|
|
649
|
+
if (s === 'completed' || s === 'failed') {
|
|
650
|
+
const removed = await queue.clean(olderThanMs, 1000, s);
|
|
651
|
+
cleaned += removed.length;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
logger.info({ queueName, cleaned, status: statuses }, 'Jobs cleaned');
|
|
656
|
+
|
|
657
|
+
return cleaned;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* Emit job event
|
|
662
|
+
*/
|
|
663
|
+
private emitEvent(event: JobEvent): void {
|
|
664
|
+
this.emit(event.event, event);
|
|
665
|
+
this.emit('job:event', event);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* List all queues
|
|
670
|
+
*/
|
|
671
|
+
async listQueues(): Promise<string[]> {
|
|
672
|
+
return Array.from(this.queues.keys());
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* Pause a queue
|
|
677
|
+
*/
|
|
678
|
+
async pauseQueue(queueName: string): Promise<void> {
|
|
679
|
+
const queue = this.queues.get(queueName);
|
|
680
|
+
if (!queue) {
|
|
681
|
+
throw new NotFoundError('Queue not found');
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
await queue.pause();
|
|
685
|
+
|
|
686
|
+
logger.info({ queueName }, 'Queue paused');
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* Resume a queue
|
|
691
|
+
*/
|
|
692
|
+
async resumeQueue(queueName: string): Promise<void> {
|
|
693
|
+
const queue = this.queues.get(queueName);
|
|
694
|
+
if (!queue) {
|
|
695
|
+
throw new NotFoundError('Queue not found');
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
await queue.resume();
|
|
699
|
+
|
|
700
|
+
logger.info({ queueName }, 'Queue resumed');
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* Drain a queue (remove all jobs)
|
|
705
|
+
*/
|
|
706
|
+
async drainQueue(queueName: string): Promise<void> {
|
|
707
|
+
const queue = this.queues.get(queueName);
|
|
708
|
+
if (!queue) {
|
|
709
|
+
throw new NotFoundError('Queue not found');
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
await queue.drain();
|
|
713
|
+
|
|
714
|
+
logger.info({ queueName }, 'Queue drained');
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* Obliterate a queue (remove queue and all data)
|
|
719
|
+
*/
|
|
720
|
+
async obliterateQueue(queueName: string): Promise<void> {
|
|
721
|
+
const queue = this.queues.get(queueName);
|
|
722
|
+
if (!queue) {
|
|
723
|
+
throw new NotFoundError('Queue not found');
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Close worker first
|
|
727
|
+
const worker = this.workers.get(queueName);
|
|
728
|
+
if (worker) {
|
|
729
|
+
await worker.close();
|
|
730
|
+
this.workers.delete(queueName);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// Close queue events
|
|
734
|
+
const queueEvents = this.queueEvents.get(queueName);
|
|
735
|
+
if (queueEvents) {
|
|
736
|
+
await queueEvents.close();
|
|
737
|
+
this.queueEvents.delete(queueName);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// Obliterate queue
|
|
741
|
+
await queue.obliterate();
|
|
742
|
+
this.queues.delete(queueName);
|
|
743
|
+
this.workerProcessors.delete(queueName);
|
|
744
|
+
this.metrics.delete(queueName);
|
|
745
|
+
|
|
746
|
+
logger.info({ queueName }, 'Queue obliterated');
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* Get job progress
|
|
751
|
+
*/
|
|
752
|
+
async getJobProgress(
|
|
753
|
+
queueName: string,
|
|
754
|
+
jobId: string
|
|
755
|
+
): Promise<number | object | string | boolean> {
|
|
756
|
+
const queue = this.queues.get(queueName);
|
|
757
|
+
if (!queue) {
|
|
758
|
+
throw new NotFoundError('Queue not found');
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
const bullJob = await queue.getJob(jobId);
|
|
762
|
+
if (!bullJob) {
|
|
763
|
+
throw new NotFoundError('Job not found');
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
return bullJob.progress;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/**
|
|
770
|
+
* Update job progress
|
|
771
|
+
*/
|
|
772
|
+
async updateJobProgress(
|
|
773
|
+
queueName: string,
|
|
774
|
+
jobId: string,
|
|
775
|
+
progress: number | object
|
|
776
|
+
): Promise<void> {
|
|
777
|
+
const queue = this.queues.get(queueName);
|
|
778
|
+
if (!queue) {
|
|
779
|
+
throw new NotFoundError('Queue not found');
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
const bullJob = await queue.getJob(jobId);
|
|
783
|
+
if (!bullJob) {
|
|
784
|
+
throw new NotFoundError('Job not found');
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
await bullJob.updateProgress(progress);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/**
|
|
791
|
+
* Graceful shutdown
|
|
792
|
+
*/
|
|
793
|
+
async close(): Promise<void> {
|
|
794
|
+
if (this.isClosing) {
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
this.isClosing = true;
|
|
799
|
+
logger.info('Closing queue service...');
|
|
800
|
+
|
|
801
|
+
// Close all workers (wait for active jobs)
|
|
802
|
+
const workerPromises = Array.from(this.workers.values()).map((worker) => worker.close());
|
|
803
|
+
await Promise.all(workerPromises);
|
|
804
|
+
|
|
805
|
+
// Close all queue events
|
|
806
|
+
const eventPromises = Array.from(this.queueEvents.values()).map((events) => events.close());
|
|
807
|
+
await Promise.all(eventPromises);
|
|
808
|
+
|
|
809
|
+
// Close all queues
|
|
810
|
+
const queuePromises = Array.from(this.queues.values()).map((queue) => queue.close());
|
|
811
|
+
await Promise.all(queuePromises);
|
|
812
|
+
|
|
813
|
+
// Close Redis connection
|
|
814
|
+
await this.connection.quit();
|
|
815
|
+
|
|
816
|
+
this.workers.clear();
|
|
817
|
+
this.queueEvents.clear();
|
|
818
|
+
this.queues.clear();
|
|
819
|
+
this.workerProcessors.clear();
|
|
820
|
+
this.metrics.clear();
|
|
821
|
+
|
|
822
|
+
logger.info('Queue service closed');
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
/**
|
|
826
|
+
* Check if service is connected
|
|
827
|
+
*/
|
|
828
|
+
isConnected(): boolean {
|
|
829
|
+
return this.connection.status === 'ready';
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/**
|
|
833
|
+
* Get Redis connection info
|
|
834
|
+
*/
|
|
835
|
+
getConnectionInfo(): { host: string; port: number; status: string } {
|
|
836
|
+
return {
|
|
837
|
+
host: this.config.redis.host || 'localhost',
|
|
838
|
+
port: this.config.redis.port || 6379,
|
|
839
|
+
status: this.connection.status,
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
}
|