@bloomneo/appkit 1.2.9
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/LICENSE +21 -0
- package/README.md +902 -0
- package/bin/appkit.js +71 -0
- package/bin/commands/generate.js +1050 -0
- package/bin/templates/backend/README.md.template +39 -0
- package/bin/templates/backend/api.http.template +0 -0
- package/bin/templates/backend/docs/APPKIT_CLI.md +507 -0
- package/bin/templates/backend/docs/APPKIT_COMMENTS_GUIDELINES.md +61 -0
- package/bin/templates/backend/docs/APPKIT_LLM_GUIDE.md +2539 -0
- package/bin/templates/backend/package.json.template +34 -0
- package/bin/templates/backend/src/api/features/welcome/welcome.http.template +29 -0
- package/bin/templates/backend/src/api/features/welcome/welcome.route.ts.template +36 -0
- package/bin/templates/backend/src/api/features/welcome/welcome.service.ts.template +88 -0
- package/bin/templates/backend/src/api/features/welcome/welcome.types.ts.template +18 -0
- package/bin/templates/backend/src/api/lib/api-router.ts.template +84 -0
- package/bin/templates/backend/src/api/server.ts.template +188 -0
- package/bin/templates/backend/tsconfig.api.json.template +24 -0
- package/bin/templates/backend/tsconfig.json.template +40 -0
- package/bin/templates/feature/feature.http.template +63 -0
- package/bin/templates/feature/feature.route.ts.template +36 -0
- package/bin/templates/feature/feature.service.ts.template +81 -0
- package/bin/templates/feature/feature.types.ts.template +23 -0
- package/bin/templates/feature-db/feature.http.template +63 -0
- package/bin/templates/feature-db/feature.model.ts.template +74 -0
- package/bin/templates/feature-db/feature.route.ts.template +58 -0
- package/bin/templates/feature-db/feature.service.ts.template +231 -0
- package/bin/templates/feature-db/feature.types.ts.template +25 -0
- package/bin/templates/feature-db/schema-addition.prisma.template +9 -0
- package/bin/templates/feature-db/seeding/README.md.template +57 -0
- package/bin/templates/feature-db/seeding/feature.seed.js.template +67 -0
- package/bin/templates/feature-user/schema-addition.prisma.template +19 -0
- package/bin/templates/feature-user/user.http.template +157 -0
- package/bin/templates/feature-user/user.model.ts.template +244 -0
- package/bin/templates/feature-user/user.route.ts.template +379 -0
- package/bin/templates/feature-user/user.seed.js.template +182 -0
- package/bin/templates/feature-user/user.service.ts.template +426 -0
- package/bin/templates/feature-user/user.types.ts.template +127 -0
- package/dist/auth/auth.d.ts +182 -0
- package/dist/auth/auth.d.ts.map +1 -0
- package/dist/auth/auth.js +477 -0
- package/dist/auth/auth.js.map +1 -0
- package/dist/auth/defaults.d.ts +104 -0
- package/dist/auth/defaults.d.ts.map +1 -0
- package/dist/auth/defaults.js +374 -0
- package/dist/auth/defaults.js.map +1 -0
- package/dist/auth/index.d.ts +70 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +94 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/cache/cache.d.ts +118 -0
- package/dist/cache/cache.d.ts.map +1 -0
- package/dist/cache/cache.js +249 -0
- package/dist/cache/cache.js.map +1 -0
- package/dist/cache/defaults.d.ts +63 -0
- package/dist/cache/defaults.d.ts.map +1 -0
- package/dist/cache/defaults.js +193 -0
- package/dist/cache/defaults.js.map +1 -0
- package/dist/cache/index.d.ts +101 -0
- package/dist/cache/index.d.ts.map +1 -0
- package/dist/cache/index.js +203 -0
- package/dist/cache/index.js.map +1 -0
- package/dist/cache/strategies/memory.d.ts +138 -0
- package/dist/cache/strategies/memory.d.ts.map +1 -0
- package/dist/cache/strategies/memory.js +348 -0
- package/dist/cache/strategies/memory.js.map +1 -0
- package/dist/cache/strategies/redis.d.ts +105 -0
- package/dist/cache/strategies/redis.d.ts.map +1 -0
- package/dist/cache/strategies/redis.js +318 -0
- package/dist/cache/strategies/redis.js.map +1 -0
- package/dist/config/config.d.ts +62 -0
- package/dist/config/config.d.ts.map +1 -0
- package/dist/config/config.js +107 -0
- package/dist/config/config.js.map +1 -0
- package/dist/config/defaults.d.ts +44 -0
- package/dist/config/defaults.d.ts.map +1 -0
- package/dist/config/defaults.js +217 -0
- package/dist/config/defaults.js.map +1 -0
- package/dist/config/index.d.ts +105 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +163 -0
- package/dist/config/index.js.map +1 -0
- package/dist/database/adapters/mongoose.d.ts +106 -0
- package/dist/database/adapters/mongoose.d.ts.map +1 -0
- package/dist/database/adapters/mongoose.js +480 -0
- package/dist/database/adapters/mongoose.js.map +1 -0
- package/dist/database/adapters/prisma.d.ts +106 -0
- package/dist/database/adapters/prisma.d.ts.map +1 -0
- package/dist/database/adapters/prisma.js +494 -0
- package/dist/database/adapters/prisma.js.map +1 -0
- package/dist/database/defaults.d.ts +87 -0
- package/dist/database/defaults.d.ts.map +1 -0
- package/dist/database/defaults.js +271 -0
- package/dist/database/defaults.js.map +1 -0
- package/dist/database/index.d.ts +137 -0
- package/dist/database/index.d.ts.map +1 -0
- package/dist/database/index.js +490 -0
- package/dist/database/index.js.map +1 -0
- package/dist/email/defaults.d.ts +100 -0
- package/dist/email/defaults.d.ts.map +1 -0
- package/dist/email/defaults.js +400 -0
- package/dist/email/defaults.js.map +1 -0
- package/dist/email/email.d.ts +139 -0
- package/dist/email/email.d.ts.map +1 -0
- package/dist/email/email.js +316 -0
- package/dist/email/email.js.map +1 -0
- package/dist/email/index.d.ts +176 -0
- package/dist/email/index.d.ts.map +1 -0
- package/dist/email/index.js +251 -0
- package/dist/email/index.js.map +1 -0
- package/dist/email/strategies/console.d.ts +90 -0
- package/dist/email/strategies/console.d.ts.map +1 -0
- package/dist/email/strategies/console.js +268 -0
- package/dist/email/strategies/console.js.map +1 -0
- package/dist/email/strategies/resend.d.ts +84 -0
- package/dist/email/strategies/resend.d.ts.map +1 -0
- package/dist/email/strategies/resend.js +266 -0
- package/dist/email/strategies/resend.js.map +1 -0
- package/dist/email/strategies/smtp.d.ts +77 -0
- package/dist/email/strategies/smtp.d.ts.map +1 -0
- package/dist/email/strategies/smtp.js +286 -0
- package/dist/email/strategies/smtp.js.map +1 -0
- package/dist/error/defaults.d.ts +40 -0
- package/dist/error/defaults.d.ts.map +1 -0
- package/dist/error/defaults.js +75 -0
- package/dist/error/defaults.js.map +1 -0
- package/dist/error/error.d.ts +140 -0
- package/dist/error/error.d.ts.map +1 -0
- package/dist/error/error.js +200 -0
- package/dist/error/error.js.map +1 -0
- package/dist/error/index.d.ts +145 -0
- package/dist/error/index.d.ts.map +1 -0
- package/dist/error/index.js +145 -0
- package/dist/error/index.js.map +1 -0
- package/dist/event/defaults.d.ts +111 -0
- package/dist/event/defaults.d.ts.map +1 -0
- package/dist/event/defaults.js +378 -0
- package/dist/event/defaults.js.map +1 -0
- package/dist/event/event.d.ts +171 -0
- package/dist/event/event.d.ts.map +1 -0
- package/dist/event/event.js +391 -0
- package/dist/event/event.js.map +1 -0
- package/dist/event/index.d.ts +173 -0
- package/dist/event/index.d.ts.map +1 -0
- package/dist/event/index.js +302 -0
- package/dist/event/index.js.map +1 -0
- package/dist/event/strategies/memory.d.ts +122 -0
- package/dist/event/strategies/memory.d.ts.map +1 -0
- package/dist/event/strategies/memory.js +331 -0
- package/dist/event/strategies/memory.js.map +1 -0
- package/dist/event/strategies/redis.d.ts +115 -0
- package/dist/event/strategies/redis.d.ts.map +1 -0
- package/dist/event/strategies/redis.js +434 -0
- package/dist/event/strategies/redis.js.map +1 -0
- package/dist/index.d.ts +58 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +72 -0
- package/dist/index.js.map +1 -0
- package/dist/logger/defaults.d.ts +67 -0
- package/dist/logger/defaults.d.ts.map +1 -0
- package/dist/logger/defaults.js +213 -0
- package/dist/logger/defaults.js.map +1 -0
- package/dist/logger/index.d.ts +84 -0
- package/dist/logger/index.d.ts.map +1 -0
- package/dist/logger/index.js +101 -0
- package/dist/logger/index.js.map +1 -0
- package/dist/logger/logger.d.ts +165 -0
- package/dist/logger/logger.d.ts.map +1 -0
- package/dist/logger/logger.js +843 -0
- package/dist/logger/logger.js.map +1 -0
- package/dist/logger/transports/console.d.ts +102 -0
- package/dist/logger/transports/console.d.ts.map +1 -0
- package/dist/logger/transports/console.js +276 -0
- package/dist/logger/transports/console.js.map +1 -0
- package/dist/logger/transports/database.d.ts +153 -0
- package/dist/logger/transports/database.d.ts.map +1 -0
- package/dist/logger/transports/database.js +539 -0
- package/dist/logger/transports/database.js.map +1 -0
- package/dist/logger/transports/file.d.ts +146 -0
- package/dist/logger/transports/file.d.ts.map +1 -0
- package/dist/logger/transports/file.js +464 -0
- package/dist/logger/transports/file.js.map +1 -0
- package/dist/logger/transports/http.d.ts +128 -0
- package/dist/logger/transports/http.d.ts.map +1 -0
- package/dist/logger/transports/http.js +401 -0
- package/dist/logger/transports/http.js.map +1 -0
- package/dist/logger/transports/webhook.d.ts +152 -0
- package/dist/logger/transports/webhook.d.ts.map +1 -0
- package/dist/logger/transports/webhook.js +485 -0
- package/dist/logger/transports/webhook.js.map +1 -0
- package/dist/queue/defaults.d.ts +66 -0
- package/dist/queue/defaults.d.ts.map +1 -0
- package/dist/queue/defaults.js +205 -0
- package/dist/queue/defaults.js.map +1 -0
- package/dist/queue/index.d.ts +124 -0
- package/dist/queue/index.d.ts.map +1 -0
- package/dist/queue/index.js +116 -0
- package/dist/queue/index.js.map +1 -0
- package/dist/queue/queue.d.ts +156 -0
- package/dist/queue/queue.d.ts.map +1 -0
- package/dist/queue/queue.js +387 -0
- package/dist/queue/queue.js.map +1 -0
- package/dist/queue/transports/database.d.ts +165 -0
- package/dist/queue/transports/database.d.ts.map +1 -0
- package/dist/queue/transports/database.js +595 -0
- package/dist/queue/transports/database.js.map +1 -0
- package/dist/queue/transports/memory.d.ts +143 -0
- package/dist/queue/transports/memory.d.ts.map +1 -0
- package/dist/queue/transports/memory.js +415 -0
- package/dist/queue/transports/memory.js.map +1 -0
- package/dist/queue/transports/redis.d.ts +203 -0
- package/dist/queue/transports/redis.d.ts.map +1 -0
- package/dist/queue/transports/redis.js +744 -0
- package/dist/queue/transports/redis.js.map +1 -0
- package/dist/security/defaults.d.ts +64 -0
- package/dist/security/defaults.d.ts.map +1 -0
- package/dist/security/defaults.js +159 -0
- package/dist/security/defaults.js.map +1 -0
- package/dist/security/index.d.ts +110 -0
- package/dist/security/index.d.ts.map +1 -0
- package/dist/security/index.js +160 -0
- package/dist/security/index.js.map +1 -0
- package/dist/security/security.d.ts +138 -0
- package/dist/security/security.d.ts.map +1 -0
- package/dist/security/security.js +419 -0
- package/dist/security/security.js.map +1 -0
- package/dist/storage/defaults.d.ts +79 -0
- package/dist/storage/defaults.d.ts.map +1 -0
- package/dist/storage/defaults.js +358 -0
- package/dist/storage/defaults.js.map +1 -0
- package/dist/storage/index.d.ts +153 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +242 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/storage.d.ts +151 -0
- package/dist/storage/storage.d.ts.map +1 -0
- package/dist/storage/storage.js +439 -0
- package/dist/storage/storage.js.map +1 -0
- package/dist/storage/strategies/local.d.ts +117 -0
- package/dist/storage/strategies/local.d.ts.map +1 -0
- package/dist/storage/strategies/local.js +368 -0
- package/dist/storage/strategies/local.js.map +1 -0
- package/dist/storage/strategies/r2.d.ts +130 -0
- package/dist/storage/strategies/r2.d.ts.map +1 -0
- package/dist/storage/strategies/r2.js +470 -0
- package/dist/storage/strategies/r2.js.map +1 -0
- package/dist/storage/strategies/s3.d.ts +121 -0
- package/dist/storage/strategies/s3.d.ts.map +1 -0
- package/dist/storage/strategies/s3.js +461 -0
- package/dist/storage/strategies/s3.js.map +1 -0
- package/dist/util/defaults.d.ts +77 -0
- package/dist/util/defaults.d.ts.map +1 -0
- package/dist/util/defaults.js +193 -0
- package/dist/util/defaults.js.map +1 -0
- package/dist/util/index.d.ts +97 -0
- package/dist/util/index.d.ts.map +1 -0
- package/dist/util/index.js +165 -0
- package/dist/util/index.js.map +1 -0
- package/dist/util/util.d.ts +145 -0
- package/dist/util/util.d.ts.map +1 -0
- package/dist/util/util.js +481 -0
- package/dist/util/util.js.map +1 -0
- package/package.json +234 -0
|
@@ -0,0 +1,744 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redis queue transport for production distributed queuing
|
|
3
|
+
* @module @bloomneo/appkit/queue
|
|
4
|
+
* @file src/queue/transports/redis.ts
|
|
5
|
+
*
|
|
6
|
+
* @llm-rule WHEN: Production environment with REDIS_URL - best for distributed systems
|
|
7
|
+
* @llm-rule AVOID: Development without Redis server - use memory transport instead
|
|
8
|
+
* @llm-rule NOTE: Persistent, distributed, high-performance with Redis data structures
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Redis transport for production distributed queuing
|
|
12
|
+
*/
|
|
13
|
+
export class RedisTransport {
|
|
14
|
+
config;
|
|
15
|
+
client = null;
|
|
16
|
+
subscriber = null;
|
|
17
|
+
connected = false;
|
|
18
|
+
handlers = new Map();
|
|
19
|
+
paused = new Set();
|
|
20
|
+
processing = new Set();
|
|
21
|
+
// Timers for background processing
|
|
22
|
+
processingLoop = null;
|
|
23
|
+
healthCheckTimer = null;
|
|
24
|
+
cleanupTimer = null;
|
|
25
|
+
/**
|
|
26
|
+
* Creates Redis transport with automatic connection management
|
|
27
|
+
* @llm-rule WHEN: Auto-detected from REDIS_URL environment variable
|
|
28
|
+
* @llm-rule AVOID: Manual Redis setup - environment detection handles this
|
|
29
|
+
*/
|
|
30
|
+
constructor(config) {
|
|
31
|
+
this.config = config;
|
|
32
|
+
this.initialize();
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Initialize Redis connection and setup
|
|
36
|
+
* @llm-rule WHEN: Transport creation - establishes Redis connection
|
|
37
|
+
* @llm-rule AVOID: Calling manually - constructor handles initialization
|
|
38
|
+
*/
|
|
39
|
+
async initialize() {
|
|
40
|
+
try {
|
|
41
|
+
await this.connect();
|
|
42
|
+
await this.setupRedisStructures();
|
|
43
|
+
if (this.config.worker.enabled) {
|
|
44
|
+
this.startProcessing();
|
|
45
|
+
this.setupCleanup();
|
|
46
|
+
this.setupHealthCheck();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
console.error('Redis transport initialization failed:', error.message);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Add job to Redis queue
|
|
55
|
+
* @llm-rule WHEN: Adding jobs for distributed background processing
|
|
56
|
+
* @llm-rule AVOID: Very large job data - Redis has memory limits
|
|
57
|
+
*/
|
|
58
|
+
async add(id, jobType, data, options) {
|
|
59
|
+
if (!this.connected) {
|
|
60
|
+
throw new Error('Redis not connected');
|
|
61
|
+
}
|
|
62
|
+
const job = {
|
|
63
|
+
id,
|
|
64
|
+
type: jobType,
|
|
65
|
+
data,
|
|
66
|
+
options,
|
|
67
|
+
status: 'waiting',
|
|
68
|
+
attempts: 0,
|
|
69
|
+
maxAttempts: options.attempts || this.config.maxAttempts,
|
|
70
|
+
createdAt: new Date().toISOString(),
|
|
71
|
+
runAt: new Date().toISOString(),
|
|
72
|
+
};
|
|
73
|
+
const jobKey = this.getJobKey(id);
|
|
74
|
+
const queueKey = this.getQueueKey(jobType, 'waiting');
|
|
75
|
+
const priority = options.priority || this.config.defaultPriority;
|
|
76
|
+
try {
|
|
77
|
+
// Use Redis transaction for atomicity
|
|
78
|
+
const multi = this.client.multi();
|
|
79
|
+
// Store job data
|
|
80
|
+
multi.hset(jobKey, 'data', JSON.stringify(job));
|
|
81
|
+
// Add to priority queue (sorted set)
|
|
82
|
+
multi.zadd(queueKey, priority, id);
|
|
83
|
+
// Add to global job set for tracking
|
|
84
|
+
multi.sadd(this.getGlobalKey('jobs'), id);
|
|
85
|
+
// Execute transaction
|
|
86
|
+
await multi.exec();
|
|
87
|
+
// Notify workers of new job
|
|
88
|
+
await this.client.publish(this.getNotificationKey(jobType), id);
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
throw new Error(`Failed to add job to Redis: ${error.message}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Register job processor
|
|
96
|
+
* @llm-rule WHEN: Setting up distributed job handlers
|
|
97
|
+
* @llm-rule AVOID: Multiple handlers for same type in same process - causes conflicts
|
|
98
|
+
*/
|
|
99
|
+
process(jobType, handler) {
|
|
100
|
+
this.handlers.set(jobType, handler);
|
|
101
|
+
// Subscribe to job notifications for this type
|
|
102
|
+
if (this.connected && this.config.worker.enabled) {
|
|
103
|
+
this.subscribeToJobType(jobType);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Schedule job for future execution in Redis
|
|
108
|
+
* @llm-rule WHEN: Need persistent delayed job execution across restarts
|
|
109
|
+
* @llm-rule AVOID: Very distant future dates - Redis memory considerations
|
|
110
|
+
*/
|
|
111
|
+
async schedule(id, jobType, data, delay) {
|
|
112
|
+
if (!this.connected) {
|
|
113
|
+
throw new Error('Redis not connected');
|
|
114
|
+
}
|
|
115
|
+
const runAt = new Date(Date.now() + delay);
|
|
116
|
+
const job = {
|
|
117
|
+
id,
|
|
118
|
+
type: jobType,
|
|
119
|
+
data,
|
|
120
|
+
options: { attempts: this.config.maxAttempts },
|
|
121
|
+
status: 'delayed',
|
|
122
|
+
attempts: 0,
|
|
123
|
+
maxAttempts: this.config.maxAttempts,
|
|
124
|
+
createdAt: new Date().toISOString(),
|
|
125
|
+
runAt: runAt.toISOString(),
|
|
126
|
+
};
|
|
127
|
+
const jobKey = this.getJobKey(id);
|
|
128
|
+
const delayedKey = this.getGlobalKey('delayed');
|
|
129
|
+
try {
|
|
130
|
+
const multi = this.client.multi();
|
|
131
|
+
// Store job data
|
|
132
|
+
multi.hset(jobKey, 'data', JSON.stringify(job));
|
|
133
|
+
// Add to delayed jobs sorted set (score = timestamp)
|
|
134
|
+
multi.zadd(delayedKey, runAt.getTime(), id);
|
|
135
|
+
// Add to global job set
|
|
136
|
+
multi.sadd(this.getGlobalKey('jobs'), id);
|
|
137
|
+
await multi.exec();
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
throw new Error(`Failed to schedule job in Redis: ${error.message}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Pause queue processing
|
|
145
|
+
* @llm-rule WHEN: Maintenance mode or controlled processing stop
|
|
146
|
+
* @llm-rule AVOID: Pausing without coordination across workers
|
|
147
|
+
*/
|
|
148
|
+
async pause(jobType) {
|
|
149
|
+
if (jobType) {
|
|
150
|
+
this.paused.add(jobType);
|
|
151
|
+
// Store pause state in Redis for coordination
|
|
152
|
+
await this.client.sadd(this.getGlobalKey('paused'), jobType);
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
// Pause all by setting global pause flag
|
|
156
|
+
await this.client.set(this.getGlobalKey('paused:all'), '1');
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Resume queue processing
|
|
161
|
+
* @llm-rule WHEN: Resuming after maintenance pause
|
|
162
|
+
* @llm-rule AVOID: Resuming without checking system health
|
|
163
|
+
*/
|
|
164
|
+
async resume(jobType) {
|
|
165
|
+
if (jobType) {
|
|
166
|
+
this.paused.delete(jobType);
|
|
167
|
+
await this.client.srem(this.getGlobalKey('paused'), jobType);
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
// Resume all
|
|
171
|
+
this.paused.clear();
|
|
172
|
+
await this.client.del(this.getGlobalKey('paused:all'));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Get queue statistics from Redis
|
|
177
|
+
* @llm-rule WHEN: Monitoring distributed queue health
|
|
178
|
+
* @llm-rule AVOID: Frequent polling - Redis operations have network cost
|
|
179
|
+
*/
|
|
180
|
+
async getStats(jobType) {
|
|
181
|
+
if (!this.connected) {
|
|
182
|
+
throw new Error('Redis not connected');
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
const multi = this.client.multi();
|
|
186
|
+
if (jobType) {
|
|
187
|
+
// Stats for specific job type
|
|
188
|
+
multi.zcard(this.getQueueKey(jobType, 'waiting'));
|
|
189
|
+
multi.zcard(this.getQueueKey(jobType, 'active'));
|
|
190
|
+
multi.zcard(this.getQueueKey(jobType, 'completed'));
|
|
191
|
+
multi.zcard(this.getQueueKey(jobType, 'failed'));
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
// Global stats across all job types
|
|
195
|
+
multi.zcard(this.getGlobalKey('waiting'));
|
|
196
|
+
multi.zcard(this.getGlobalKey('active'));
|
|
197
|
+
multi.zcard(this.getGlobalKey('completed'));
|
|
198
|
+
multi.zcard(this.getGlobalKey('failed'));
|
|
199
|
+
}
|
|
200
|
+
multi.zcard(this.getGlobalKey('delayed'));
|
|
201
|
+
multi.scard(this.getGlobalKey('paused'));
|
|
202
|
+
const results = await multi.exec();
|
|
203
|
+
return {
|
|
204
|
+
waiting: results[0][1] || 0,
|
|
205
|
+
active: results[1][1] || 0,
|
|
206
|
+
completed: results[2][1] || 0,
|
|
207
|
+
failed: results[3][1] || 0,
|
|
208
|
+
delayed: results[4][1] || 0,
|
|
209
|
+
paused: results[5][1] || 0,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
throw new Error(`Failed to get Redis stats: ${error.message}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Get jobs by status from Redis
|
|
218
|
+
* @llm-rule WHEN: Debugging distributed queue issues
|
|
219
|
+
* @llm-rule AVOID: Getting large result sets - use pagination for production
|
|
220
|
+
*/
|
|
221
|
+
async getJobs(status, jobType, limit = 100) {
|
|
222
|
+
if (!this.connected) {
|
|
223
|
+
throw new Error('Redis not connected');
|
|
224
|
+
}
|
|
225
|
+
try {
|
|
226
|
+
const queueKey = jobType
|
|
227
|
+
? this.getQueueKey(jobType, status)
|
|
228
|
+
: this.getGlobalKey(status);
|
|
229
|
+
// Get job IDs from sorted set (newest first)
|
|
230
|
+
const jobIds = await this.client.zrevrange(queueKey, 0, limit - 1);
|
|
231
|
+
if (jobIds.length === 0) {
|
|
232
|
+
return [];
|
|
233
|
+
}
|
|
234
|
+
// Get job data for all IDs
|
|
235
|
+
const jobs = [];
|
|
236
|
+
for (const id of jobIds) {
|
|
237
|
+
const jobData = await this.getJobData(id);
|
|
238
|
+
if (jobData) {
|
|
239
|
+
jobs.push(this.redisJobToInfo(jobData));
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return jobs;
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
throw new Error(`Failed to get Redis jobs: ${error.message}`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Retry failed job in Redis
|
|
250
|
+
* @llm-rule WHEN: Manual retry of failed distributed jobs
|
|
251
|
+
* @llm-rule AVOID: Retrying without fixing underlying issues
|
|
252
|
+
*/
|
|
253
|
+
async retry(jobId) {
|
|
254
|
+
if (!this.connected) {
|
|
255
|
+
throw new Error('Redis not connected');
|
|
256
|
+
}
|
|
257
|
+
try {
|
|
258
|
+
const job = await this.getJobData(jobId);
|
|
259
|
+
if (!job) {
|
|
260
|
+
throw new Error(`Job ${jobId} not found`);
|
|
261
|
+
}
|
|
262
|
+
if (job.status !== 'failed') {
|
|
263
|
+
throw new Error(`Job ${jobId} is not in failed state`);
|
|
264
|
+
}
|
|
265
|
+
// Reset job for retry
|
|
266
|
+
job.status = 'waiting';
|
|
267
|
+
job.attempts = 0;
|
|
268
|
+
job.error = undefined;
|
|
269
|
+
job.failedAt = undefined;
|
|
270
|
+
job.runAt = new Date().toISOString();
|
|
271
|
+
const multi = this.client.multi();
|
|
272
|
+
// Update job data
|
|
273
|
+
multi.hset(this.getJobKey(jobId), 'data', JSON.stringify(job));
|
|
274
|
+
// Move from failed to waiting queue
|
|
275
|
+
multi.zrem(this.getQueueKey(job.type, 'failed'), jobId);
|
|
276
|
+
multi.zadd(this.getQueueKey(job.type, 'waiting'), job.options.priority || 0, jobId);
|
|
277
|
+
await multi.exec();
|
|
278
|
+
// Notify workers
|
|
279
|
+
await this.client.publish(this.getNotificationKey(job.type), jobId);
|
|
280
|
+
}
|
|
281
|
+
catch (error) {
|
|
282
|
+
throw new Error(`Failed to retry Redis job: ${error.message}`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Remove job from Redis
|
|
287
|
+
* @llm-rule WHEN: Canceling scheduled jobs or cleanup
|
|
288
|
+
* @llm-rule AVOID: Removing active jobs - can cause worker inconsistencies
|
|
289
|
+
*/
|
|
290
|
+
async remove(jobId) {
|
|
291
|
+
if (!this.connected) {
|
|
292
|
+
throw new Error('Redis not connected');
|
|
293
|
+
}
|
|
294
|
+
try {
|
|
295
|
+
const job = await this.getJobData(jobId);
|
|
296
|
+
if (!job) {
|
|
297
|
+
throw new Error(`Job ${jobId} not found`);
|
|
298
|
+
}
|
|
299
|
+
if (job.status === 'active') {
|
|
300
|
+
throw new Error(`Cannot remove active job ${jobId}`);
|
|
301
|
+
}
|
|
302
|
+
const multi = this.client.multi();
|
|
303
|
+
// Remove job data
|
|
304
|
+
multi.del(this.getJobKey(jobId));
|
|
305
|
+
// Remove from all possible queues
|
|
306
|
+
multi.zrem(this.getQueueKey(job.type, job.status), jobId);
|
|
307
|
+
multi.zrem(this.getGlobalKey('delayed'), jobId);
|
|
308
|
+
multi.srem(this.getGlobalKey('jobs'), jobId);
|
|
309
|
+
await multi.exec();
|
|
310
|
+
}
|
|
311
|
+
catch (error) {
|
|
312
|
+
throw new Error(`Failed to remove Redis job: ${error.message}`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Clean old jobs from Redis
|
|
317
|
+
* @llm-rule WHEN: Periodic cleanup to prevent Redis memory growth
|
|
318
|
+
* @llm-rule AVOID: Aggressive cleanup without considering debugging needs
|
|
319
|
+
*/
|
|
320
|
+
async clean(status, grace = 24 * 60 * 60 * 1000) {
|
|
321
|
+
if (!this.connected) {
|
|
322
|
+
throw new Error('Redis not connected');
|
|
323
|
+
}
|
|
324
|
+
try {
|
|
325
|
+
const cutoff = Date.now() - grace;
|
|
326
|
+
const queueKey = this.getGlobalKey(status);
|
|
327
|
+
// Get old job IDs
|
|
328
|
+
const oldJobIds = await this.client.zrangebyscore(queueKey, 0, cutoff);
|
|
329
|
+
if (oldJobIds.length === 0) {
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
const multi = this.client.multi();
|
|
333
|
+
// Remove old jobs
|
|
334
|
+
for (const jobId of oldJobIds) {
|
|
335
|
+
multi.del(this.getJobKey(jobId));
|
|
336
|
+
multi.zrem(queueKey, jobId);
|
|
337
|
+
multi.srem(this.getGlobalKey('jobs'), jobId);
|
|
338
|
+
}
|
|
339
|
+
await multi.exec();
|
|
340
|
+
}
|
|
341
|
+
catch (error) {
|
|
342
|
+
throw new Error(`Failed to clean Redis jobs: ${error.message}`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Get Redis transport health status
|
|
347
|
+
* @llm-rule WHEN: Health checks and monitoring
|
|
348
|
+
* @llm-rule AVOID: Complex health logic - Redis connection is main indicator
|
|
349
|
+
*/
|
|
350
|
+
getHealth() {
|
|
351
|
+
if (!this.connected) {
|
|
352
|
+
return { status: 'unhealthy', message: 'Redis not connected' };
|
|
353
|
+
}
|
|
354
|
+
try {
|
|
355
|
+
// Check if Redis is responsive (this will throw if connection issues)
|
|
356
|
+
this.client.ping();
|
|
357
|
+
return { status: 'healthy' };
|
|
358
|
+
}
|
|
359
|
+
catch (error) {
|
|
360
|
+
return { status: 'degraded', message: 'Redis connection issues' };
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Close Redis transport and cleanup connections
|
|
365
|
+
* @llm-rule WHEN: App shutdown or testing cleanup
|
|
366
|
+
* @llm-rule AVOID: Abrupt close - finish processing current jobs first
|
|
367
|
+
*/
|
|
368
|
+
async close() {
|
|
369
|
+
// Stop processing loops
|
|
370
|
+
if (this.processingLoop) {
|
|
371
|
+
clearTimeout(this.processingLoop);
|
|
372
|
+
this.processingLoop = null;
|
|
373
|
+
}
|
|
374
|
+
if (this.healthCheckTimer) {
|
|
375
|
+
clearInterval(this.healthCheckTimer);
|
|
376
|
+
this.healthCheckTimer = null;
|
|
377
|
+
}
|
|
378
|
+
if (this.cleanupTimer) {
|
|
379
|
+
clearInterval(this.cleanupTimer);
|
|
380
|
+
this.cleanupTimer = null;
|
|
381
|
+
}
|
|
382
|
+
// Wait for current jobs to complete
|
|
383
|
+
const timeout = this.config.worker.gracefulShutdownTimeout;
|
|
384
|
+
const startTime = Date.now();
|
|
385
|
+
while (this.processing.size > 0 && Date.now() - startTime < timeout) {
|
|
386
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
387
|
+
}
|
|
388
|
+
// Close Redis connections
|
|
389
|
+
try {
|
|
390
|
+
if (this.subscriber) {
|
|
391
|
+
await this.subscriber.quit();
|
|
392
|
+
}
|
|
393
|
+
if (this.client) {
|
|
394
|
+
await this.client.quit();
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
catch (error) {
|
|
398
|
+
console.error('Error closing Redis connections:', error.message);
|
|
399
|
+
}
|
|
400
|
+
this.connected = false;
|
|
401
|
+
this.handlers.clear();
|
|
402
|
+
this.paused.clear();
|
|
403
|
+
this.processing.clear();
|
|
404
|
+
}
|
|
405
|
+
// ============================================================================
|
|
406
|
+
// PRIVATE REDIS CONNECTION METHODS
|
|
407
|
+
// ============================================================================
|
|
408
|
+
/**
|
|
409
|
+
* Connect to Redis with automatic retries
|
|
410
|
+
*/
|
|
411
|
+
async connect() {
|
|
412
|
+
try {
|
|
413
|
+
// Dynamic import of Redis client - handle both CommonJS and ES modules
|
|
414
|
+
const ioredis = await import('ioredis');
|
|
415
|
+
const Redis = ioredis.default || ioredis;
|
|
416
|
+
const redisOptions = {
|
|
417
|
+
connectTimeout: 10000,
|
|
418
|
+
retryDelayOnFailover: this.config.redis.retryDelayOnFailover,
|
|
419
|
+
maxRetriesPerRequest: this.config.redis.maxRetriesPerRequest,
|
|
420
|
+
keyPrefix: this.config.redis.keyPrefix + ':',
|
|
421
|
+
};
|
|
422
|
+
// Use type assertion to bypass TypeScript constructor checking
|
|
423
|
+
this.client = new Redis(this.config.redis.url, redisOptions);
|
|
424
|
+
this.subscriber = new Redis(this.config.redis.url, redisOptions);
|
|
425
|
+
// Wait for connection
|
|
426
|
+
await this.client.ping();
|
|
427
|
+
await this.subscriber.ping();
|
|
428
|
+
this.connected = true;
|
|
429
|
+
console.log('Redis transport connected successfully');
|
|
430
|
+
}
|
|
431
|
+
catch (error) {
|
|
432
|
+
this.connected = false;
|
|
433
|
+
throw new Error(`Redis connection failed: ${error.message}`);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Setup Redis data structures and indexes
|
|
438
|
+
*/
|
|
439
|
+
async setupRedisStructures() {
|
|
440
|
+
// Redis structures are created on demand
|
|
441
|
+
// No explicit setup needed for basic operations
|
|
442
|
+
}
|
|
443
|
+
// ============================================================================
|
|
444
|
+
// PRIVATE PROCESSING METHODS
|
|
445
|
+
// ============================================================================
|
|
446
|
+
/**
|
|
447
|
+
* Start background job processing
|
|
448
|
+
*/
|
|
449
|
+
startProcessing() {
|
|
450
|
+
this.processJobs();
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Main job processing loop
|
|
454
|
+
*/
|
|
455
|
+
async processJobs() {
|
|
456
|
+
try {
|
|
457
|
+
// Promote delayed jobs that are ready
|
|
458
|
+
await this.promoteDelayedJobs();
|
|
459
|
+
// Process waiting jobs
|
|
460
|
+
await this.processWaitingJobs();
|
|
461
|
+
}
|
|
462
|
+
catch (error) {
|
|
463
|
+
console.error('Redis processing error:', error.message);
|
|
464
|
+
}
|
|
465
|
+
// Schedule next processing cycle
|
|
466
|
+
this.processingLoop = setTimeout(() => this.processJobs(), 2000);
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Promote delayed jobs that are ready to run
|
|
470
|
+
*/
|
|
471
|
+
async promoteDelayedJobs() {
|
|
472
|
+
if (!this.connected)
|
|
473
|
+
return;
|
|
474
|
+
try {
|
|
475
|
+
const now = Date.now();
|
|
476
|
+
const delayedKey = this.getGlobalKey('delayed');
|
|
477
|
+
// Get ready jobs (score <= now)
|
|
478
|
+
const readyJobIds = await this.client.zrangebyscore(delayedKey, 0, now);
|
|
479
|
+
for (const jobId of readyJobIds) {
|
|
480
|
+
const job = await this.getJobData(jobId);
|
|
481
|
+
if (job && job.status === 'delayed') {
|
|
482
|
+
await this.promoteJob(job);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
catch (error) {
|
|
487
|
+
console.error('Error promoting delayed jobs:', error.message);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Promote single delayed job to waiting
|
|
492
|
+
*/
|
|
493
|
+
async promoteJob(job) {
|
|
494
|
+
const multi = this.client.multi();
|
|
495
|
+
// Update job status
|
|
496
|
+
job.status = 'waiting';
|
|
497
|
+
multi.hset(this.getJobKey(job.id), 'data', JSON.stringify(job));
|
|
498
|
+
// Move from delayed to waiting queue
|
|
499
|
+
multi.zrem(this.getGlobalKey('delayed'), job.id);
|
|
500
|
+
multi.zadd(this.getQueueKey(job.type, 'waiting'), job.options.priority || 0, job.id);
|
|
501
|
+
await multi.exec();
|
|
502
|
+
// Notify workers
|
|
503
|
+
await this.client.publish(this.getNotificationKey(job.type), job.id);
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Process waiting jobs up to concurrency limit
|
|
507
|
+
*/
|
|
508
|
+
async processWaitingJobs() {
|
|
509
|
+
const concurrency = this.config.concurrency;
|
|
510
|
+
const currentActive = this.processing.size;
|
|
511
|
+
if (currentActive >= concurrency) {
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
// Process jobs for each registered handler
|
|
515
|
+
for (const [jobType, handler] of this.handlers) {
|
|
516
|
+
if (this.paused.has(jobType))
|
|
517
|
+
continue;
|
|
518
|
+
const available = concurrency - this.processing.size;
|
|
519
|
+
if (available <= 0)
|
|
520
|
+
break;
|
|
521
|
+
await this.processJobType(jobType, handler, available);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Process jobs for specific job type
|
|
526
|
+
*/
|
|
527
|
+
async processJobType(jobType, handler, limit) {
|
|
528
|
+
try {
|
|
529
|
+
const queueKey = this.getQueueKey(jobType, 'waiting');
|
|
530
|
+
// Get highest priority jobs
|
|
531
|
+
const jobIds = await this.client.zrevrange(queueKey, 0, limit - 1);
|
|
532
|
+
for (const jobId of jobIds) {
|
|
533
|
+
if (this.processing.size >= this.config.concurrency)
|
|
534
|
+
break;
|
|
535
|
+
const job = await this.getJobData(jobId);
|
|
536
|
+
if (job && job.status === 'waiting') {
|
|
537
|
+
this.processJob(job, handler).catch(error => {
|
|
538
|
+
console.error(`Error processing Redis job ${jobId}:`, error);
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
catch (error) {
|
|
544
|
+
console.error(`Error processing job type ${jobType}:`, error.message);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* Process individual job with Redis state management
|
|
549
|
+
*/
|
|
550
|
+
async processJob(job, handler) {
|
|
551
|
+
// Mark as processing
|
|
552
|
+
this.processing.add(job.id);
|
|
553
|
+
try {
|
|
554
|
+
// Move job to active queue
|
|
555
|
+
await this.moveJobToActive(job);
|
|
556
|
+
// Execute handler
|
|
557
|
+
const result = await handler(job.data);
|
|
558
|
+
// Job completed successfully
|
|
559
|
+
await this.completeJob(job, result);
|
|
560
|
+
}
|
|
561
|
+
catch (error) {
|
|
562
|
+
// Job failed
|
|
563
|
+
await this.failJob(job, error);
|
|
564
|
+
}
|
|
565
|
+
finally {
|
|
566
|
+
this.processing.delete(job.id);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* Move job to active queue
|
|
571
|
+
*/
|
|
572
|
+
async moveJobToActive(job) {
|
|
573
|
+
job.status = 'active';
|
|
574
|
+
job.processedAt = new Date().toISOString();
|
|
575
|
+
job.attempts++;
|
|
576
|
+
const multi = this.client.multi();
|
|
577
|
+
// Update job data
|
|
578
|
+
multi.hset(this.getJobKey(job.id), 'data', JSON.stringify(job));
|
|
579
|
+
// Move from waiting to active
|
|
580
|
+
multi.zrem(this.getQueueKey(job.type, 'waiting'), job.id);
|
|
581
|
+
multi.zadd(this.getQueueKey(job.type, 'active'), Date.now(), job.id);
|
|
582
|
+
await multi.exec();
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Complete job successfully
|
|
586
|
+
*/
|
|
587
|
+
async completeJob(job, result) {
|
|
588
|
+
job.status = 'completed';
|
|
589
|
+
job.completedAt = new Date().toISOString();
|
|
590
|
+
if (result !== undefined) {
|
|
591
|
+
job.result = result;
|
|
592
|
+
}
|
|
593
|
+
const multi = this.client.multi();
|
|
594
|
+
// Update job data
|
|
595
|
+
multi.hset(this.getJobKey(job.id), 'data', JSON.stringify(job));
|
|
596
|
+
// Move from active to completed
|
|
597
|
+
multi.zrem(this.getQueueKey(job.type, 'active'), job.id);
|
|
598
|
+
multi.zadd(this.getQueueKey(job.type, 'completed'), Date.now(), job.id);
|
|
599
|
+
await multi.exec();
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* Fail job with retry logic
|
|
603
|
+
*/
|
|
604
|
+
async failJob(job, error) {
|
|
605
|
+
job.error = {
|
|
606
|
+
message: error.message,
|
|
607
|
+
stack: error.stack,
|
|
608
|
+
name: error.name,
|
|
609
|
+
};
|
|
610
|
+
if (job.attempts < job.maxAttempts) {
|
|
611
|
+
// Retry with backoff
|
|
612
|
+
job.status = 'waiting';
|
|
613
|
+
job.runAt = this.calculateRetryDelay(job).toISOString();
|
|
614
|
+
const multi = this.client.multi();
|
|
615
|
+
multi.hset(this.getJobKey(job.id), 'data', JSON.stringify(job));
|
|
616
|
+
multi.zrem(this.getQueueKey(job.type, 'active'), job.id);
|
|
617
|
+
multi.zadd(this.getQueueKey(job.type, 'waiting'), job.options.priority || 0, job.id);
|
|
618
|
+
await multi.exec();
|
|
619
|
+
}
|
|
620
|
+
else {
|
|
621
|
+
// Max attempts reached
|
|
622
|
+
job.status = 'failed';
|
|
623
|
+
job.failedAt = new Date().toISOString();
|
|
624
|
+
const multi = this.client.multi();
|
|
625
|
+
multi.hset(this.getJobKey(job.id), 'data', JSON.stringify(job));
|
|
626
|
+
multi.zrem(this.getQueueKey(job.type, 'active'), job.id);
|
|
627
|
+
multi.zadd(this.getQueueKey(job.type, 'failed'), Date.now(), job.id);
|
|
628
|
+
await multi.exec();
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Calculate retry delay with backoff
|
|
633
|
+
*/
|
|
634
|
+
calculateRetryDelay(job) {
|
|
635
|
+
const baseDelay = this.config.retryDelay;
|
|
636
|
+
let delay = baseDelay;
|
|
637
|
+
if (this.config.retryBackoff === 'exponential') {
|
|
638
|
+
delay = baseDelay * Math.pow(2, job.attempts - 1);
|
|
639
|
+
}
|
|
640
|
+
// Add jitter
|
|
641
|
+
const jitter = delay * 0.25 * (Math.random() - 0.5);
|
|
642
|
+
delay += jitter;
|
|
643
|
+
return new Date(Date.now() + delay);
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Subscribe to job notifications for a job type
|
|
647
|
+
*/
|
|
648
|
+
subscribeToJobType(jobType) {
|
|
649
|
+
if (!this.subscriber)
|
|
650
|
+
return;
|
|
651
|
+
const channel = this.getNotificationKey(jobType);
|
|
652
|
+
this.subscriber.subscribe(channel);
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* Setup periodic health checks
|
|
656
|
+
*/
|
|
657
|
+
setupHealthCheck() {
|
|
658
|
+
this.healthCheckTimer = setInterval(async () => {
|
|
659
|
+
try {
|
|
660
|
+
await this.client.ping();
|
|
661
|
+
}
|
|
662
|
+
catch (error) {
|
|
663
|
+
console.error('Redis health check failed:', error.message);
|
|
664
|
+
this.connected = false;
|
|
665
|
+
}
|
|
666
|
+
}, 30000);
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* Setup periodic cleanup
|
|
670
|
+
*/
|
|
671
|
+
setupCleanup() {
|
|
672
|
+
this.cleanupTimer = setInterval(async () => {
|
|
673
|
+
try {
|
|
674
|
+
// Clean completed jobs older than 1 hour
|
|
675
|
+
await this.clean('completed', 60 * 60 * 1000);
|
|
676
|
+
// Clean failed jobs older than 24 hours
|
|
677
|
+
await this.clean('failed', 24 * 60 * 60 * 1000);
|
|
678
|
+
}
|
|
679
|
+
catch (error) {
|
|
680
|
+
console.error('Redis cleanup error:', error.message);
|
|
681
|
+
}
|
|
682
|
+
}, 60 * 60 * 1000); // Every hour
|
|
683
|
+
}
|
|
684
|
+
// ============================================================================
|
|
685
|
+
// PRIVATE UTILITY METHODS
|
|
686
|
+
// ============================================================================
|
|
687
|
+
/**
|
|
688
|
+
* Get Redis key for job data
|
|
689
|
+
*/
|
|
690
|
+
getJobKey(jobId) {
|
|
691
|
+
return `job:${jobId}`;
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* Get Redis key for specific queue
|
|
695
|
+
*/
|
|
696
|
+
getQueueKey(jobType, status) {
|
|
697
|
+
return `queue:${jobType}:${status}`;
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Get Redis key for global queues
|
|
701
|
+
*/
|
|
702
|
+
getGlobalKey(suffix) {
|
|
703
|
+
return `global:${suffix}`;
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* Get Redis key for job notifications
|
|
707
|
+
*/
|
|
708
|
+
getNotificationKey(jobType) {
|
|
709
|
+
return `notify:${jobType}`;
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* Get job data from Redis
|
|
713
|
+
*/
|
|
714
|
+
async getJobData(jobId) {
|
|
715
|
+
try {
|
|
716
|
+
const data = await this.client.hget(this.getJobKey(jobId), 'data');
|
|
717
|
+
return data ? JSON.parse(data) : null;
|
|
718
|
+
}
|
|
719
|
+
catch (error) {
|
|
720
|
+
console.error(`Error getting job data for ${jobId}:`, error.message);
|
|
721
|
+
return null;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* Convert RedisJob to JobInfo
|
|
726
|
+
*/
|
|
727
|
+
redisJobToInfo(job) {
|
|
728
|
+
return {
|
|
729
|
+
id: job.id,
|
|
730
|
+
type: job.type,
|
|
731
|
+
data: job.data,
|
|
732
|
+
status: job.status,
|
|
733
|
+
progress: job.progress,
|
|
734
|
+
attempts: job.attempts,
|
|
735
|
+
maxAttempts: job.maxAttempts,
|
|
736
|
+
error: job.error,
|
|
737
|
+
createdAt: new Date(job.createdAt),
|
|
738
|
+
processedAt: job.processedAt ? new Date(job.processedAt) : undefined,
|
|
739
|
+
completedAt: job.completedAt ? new Date(job.completedAt) : undefined,
|
|
740
|
+
failedAt: job.failedAt ? new Date(job.failedAt) : undefined,
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
//# sourceMappingURL=redis.js.map
|