@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.
Files changed (262) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +902 -0
  3. package/bin/appkit.js +71 -0
  4. package/bin/commands/generate.js +1050 -0
  5. package/bin/templates/backend/README.md.template +39 -0
  6. package/bin/templates/backend/api.http.template +0 -0
  7. package/bin/templates/backend/docs/APPKIT_CLI.md +507 -0
  8. package/bin/templates/backend/docs/APPKIT_COMMENTS_GUIDELINES.md +61 -0
  9. package/bin/templates/backend/docs/APPKIT_LLM_GUIDE.md +2539 -0
  10. package/bin/templates/backend/package.json.template +34 -0
  11. package/bin/templates/backend/src/api/features/welcome/welcome.http.template +29 -0
  12. package/bin/templates/backend/src/api/features/welcome/welcome.route.ts.template +36 -0
  13. package/bin/templates/backend/src/api/features/welcome/welcome.service.ts.template +88 -0
  14. package/bin/templates/backend/src/api/features/welcome/welcome.types.ts.template +18 -0
  15. package/bin/templates/backend/src/api/lib/api-router.ts.template +84 -0
  16. package/bin/templates/backend/src/api/server.ts.template +188 -0
  17. package/bin/templates/backend/tsconfig.api.json.template +24 -0
  18. package/bin/templates/backend/tsconfig.json.template +40 -0
  19. package/bin/templates/feature/feature.http.template +63 -0
  20. package/bin/templates/feature/feature.route.ts.template +36 -0
  21. package/bin/templates/feature/feature.service.ts.template +81 -0
  22. package/bin/templates/feature/feature.types.ts.template +23 -0
  23. package/bin/templates/feature-db/feature.http.template +63 -0
  24. package/bin/templates/feature-db/feature.model.ts.template +74 -0
  25. package/bin/templates/feature-db/feature.route.ts.template +58 -0
  26. package/bin/templates/feature-db/feature.service.ts.template +231 -0
  27. package/bin/templates/feature-db/feature.types.ts.template +25 -0
  28. package/bin/templates/feature-db/schema-addition.prisma.template +9 -0
  29. package/bin/templates/feature-db/seeding/README.md.template +57 -0
  30. package/bin/templates/feature-db/seeding/feature.seed.js.template +67 -0
  31. package/bin/templates/feature-user/schema-addition.prisma.template +19 -0
  32. package/bin/templates/feature-user/user.http.template +157 -0
  33. package/bin/templates/feature-user/user.model.ts.template +244 -0
  34. package/bin/templates/feature-user/user.route.ts.template +379 -0
  35. package/bin/templates/feature-user/user.seed.js.template +182 -0
  36. package/bin/templates/feature-user/user.service.ts.template +426 -0
  37. package/bin/templates/feature-user/user.types.ts.template +127 -0
  38. package/dist/auth/auth.d.ts +182 -0
  39. package/dist/auth/auth.d.ts.map +1 -0
  40. package/dist/auth/auth.js +477 -0
  41. package/dist/auth/auth.js.map +1 -0
  42. package/dist/auth/defaults.d.ts +104 -0
  43. package/dist/auth/defaults.d.ts.map +1 -0
  44. package/dist/auth/defaults.js +374 -0
  45. package/dist/auth/defaults.js.map +1 -0
  46. package/dist/auth/index.d.ts +70 -0
  47. package/dist/auth/index.d.ts.map +1 -0
  48. package/dist/auth/index.js +94 -0
  49. package/dist/auth/index.js.map +1 -0
  50. package/dist/cache/cache.d.ts +118 -0
  51. package/dist/cache/cache.d.ts.map +1 -0
  52. package/dist/cache/cache.js +249 -0
  53. package/dist/cache/cache.js.map +1 -0
  54. package/dist/cache/defaults.d.ts +63 -0
  55. package/dist/cache/defaults.d.ts.map +1 -0
  56. package/dist/cache/defaults.js +193 -0
  57. package/dist/cache/defaults.js.map +1 -0
  58. package/dist/cache/index.d.ts +101 -0
  59. package/dist/cache/index.d.ts.map +1 -0
  60. package/dist/cache/index.js +203 -0
  61. package/dist/cache/index.js.map +1 -0
  62. package/dist/cache/strategies/memory.d.ts +138 -0
  63. package/dist/cache/strategies/memory.d.ts.map +1 -0
  64. package/dist/cache/strategies/memory.js +348 -0
  65. package/dist/cache/strategies/memory.js.map +1 -0
  66. package/dist/cache/strategies/redis.d.ts +105 -0
  67. package/dist/cache/strategies/redis.d.ts.map +1 -0
  68. package/dist/cache/strategies/redis.js +318 -0
  69. package/dist/cache/strategies/redis.js.map +1 -0
  70. package/dist/config/config.d.ts +62 -0
  71. package/dist/config/config.d.ts.map +1 -0
  72. package/dist/config/config.js +107 -0
  73. package/dist/config/config.js.map +1 -0
  74. package/dist/config/defaults.d.ts +44 -0
  75. package/dist/config/defaults.d.ts.map +1 -0
  76. package/dist/config/defaults.js +217 -0
  77. package/dist/config/defaults.js.map +1 -0
  78. package/dist/config/index.d.ts +105 -0
  79. package/dist/config/index.d.ts.map +1 -0
  80. package/dist/config/index.js +163 -0
  81. package/dist/config/index.js.map +1 -0
  82. package/dist/database/adapters/mongoose.d.ts +106 -0
  83. package/dist/database/adapters/mongoose.d.ts.map +1 -0
  84. package/dist/database/adapters/mongoose.js +480 -0
  85. package/dist/database/adapters/mongoose.js.map +1 -0
  86. package/dist/database/adapters/prisma.d.ts +106 -0
  87. package/dist/database/adapters/prisma.d.ts.map +1 -0
  88. package/dist/database/adapters/prisma.js +494 -0
  89. package/dist/database/adapters/prisma.js.map +1 -0
  90. package/dist/database/defaults.d.ts +87 -0
  91. package/dist/database/defaults.d.ts.map +1 -0
  92. package/dist/database/defaults.js +271 -0
  93. package/dist/database/defaults.js.map +1 -0
  94. package/dist/database/index.d.ts +137 -0
  95. package/dist/database/index.d.ts.map +1 -0
  96. package/dist/database/index.js +490 -0
  97. package/dist/database/index.js.map +1 -0
  98. package/dist/email/defaults.d.ts +100 -0
  99. package/dist/email/defaults.d.ts.map +1 -0
  100. package/dist/email/defaults.js +400 -0
  101. package/dist/email/defaults.js.map +1 -0
  102. package/dist/email/email.d.ts +139 -0
  103. package/dist/email/email.d.ts.map +1 -0
  104. package/dist/email/email.js +316 -0
  105. package/dist/email/email.js.map +1 -0
  106. package/dist/email/index.d.ts +176 -0
  107. package/dist/email/index.d.ts.map +1 -0
  108. package/dist/email/index.js +251 -0
  109. package/dist/email/index.js.map +1 -0
  110. package/dist/email/strategies/console.d.ts +90 -0
  111. package/dist/email/strategies/console.d.ts.map +1 -0
  112. package/dist/email/strategies/console.js +268 -0
  113. package/dist/email/strategies/console.js.map +1 -0
  114. package/dist/email/strategies/resend.d.ts +84 -0
  115. package/dist/email/strategies/resend.d.ts.map +1 -0
  116. package/dist/email/strategies/resend.js +266 -0
  117. package/dist/email/strategies/resend.js.map +1 -0
  118. package/dist/email/strategies/smtp.d.ts +77 -0
  119. package/dist/email/strategies/smtp.d.ts.map +1 -0
  120. package/dist/email/strategies/smtp.js +286 -0
  121. package/dist/email/strategies/smtp.js.map +1 -0
  122. package/dist/error/defaults.d.ts +40 -0
  123. package/dist/error/defaults.d.ts.map +1 -0
  124. package/dist/error/defaults.js +75 -0
  125. package/dist/error/defaults.js.map +1 -0
  126. package/dist/error/error.d.ts +140 -0
  127. package/dist/error/error.d.ts.map +1 -0
  128. package/dist/error/error.js +200 -0
  129. package/dist/error/error.js.map +1 -0
  130. package/dist/error/index.d.ts +145 -0
  131. package/dist/error/index.d.ts.map +1 -0
  132. package/dist/error/index.js +145 -0
  133. package/dist/error/index.js.map +1 -0
  134. package/dist/event/defaults.d.ts +111 -0
  135. package/dist/event/defaults.d.ts.map +1 -0
  136. package/dist/event/defaults.js +378 -0
  137. package/dist/event/defaults.js.map +1 -0
  138. package/dist/event/event.d.ts +171 -0
  139. package/dist/event/event.d.ts.map +1 -0
  140. package/dist/event/event.js +391 -0
  141. package/dist/event/event.js.map +1 -0
  142. package/dist/event/index.d.ts +173 -0
  143. package/dist/event/index.d.ts.map +1 -0
  144. package/dist/event/index.js +302 -0
  145. package/dist/event/index.js.map +1 -0
  146. package/dist/event/strategies/memory.d.ts +122 -0
  147. package/dist/event/strategies/memory.d.ts.map +1 -0
  148. package/dist/event/strategies/memory.js +331 -0
  149. package/dist/event/strategies/memory.js.map +1 -0
  150. package/dist/event/strategies/redis.d.ts +115 -0
  151. package/dist/event/strategies/redis.d.ts.map +1 -0
  152. package/dist/event/strategies/redis.js +434 -0
  153. package/dist/event/strategies/redis.js.map +1 -0
  154. package/dist/index.d.ts +58 -0
  155. package/dist/index.d.ts.map +1 -0
  156. package/dist/index.js +72 -0
  157. package/dist/index.js.map +1 -0
  158. package/dist/logger/defaults.d.ts +67 -0
  159. package/dist/logger/defaults.d.ts.map +1 -0
  160. package/dist/logger/defaults.js +213 -0
  161. package/dist/logger/defaults.js.map +1 -0
  162. package/dist/logger/index.d.ts +84 -0
  163. package/dist/logger/index.d.ts.map +1 -0
  164. package/dist/logger/index.js +101 -0
  165. package/dist/logger/index.js.map +1 -0
  166. package/dist/logger/logger.d.ts +165 -0
  167. package/dist/logger/logger.d.ts.map +1 -0
  168. package/dist/logger/logger.js +843 -0
  169. package/dist/logger/logger.js.map +1 -0
  170. package/dist/logger/transports/console.d.ts +102 -0
  171. package/dist/logger/transports/console.d.ts.map +1 -0
  172. package/dist/logger/transports/console.js +276 -0
  173. package/dist/logger/transports/console.js.map +1 -0
  174. package/dist/logger/transports/database.d.ts +153 -0
  175. package/dist/logger/transports/database.d.ts.map +1 -0
  176. package/dist/logger/transports/database.js +539 -0
  177. package/dist/logger/transports/database.js.map +1 -0
  178. package/dist/logger/transports/file.d.ts +146 -0
  179. package/dist/logger/transports/file.d.ts.map +1 -0
  180. package/dist/logger/transports/file.js +464 -0
  181. package/dist/logger/transports/file.js.map +1 -0
  182. package/dist/logger/transports/http.d.ts +128 -0
  183. package/dist/logger/transports/http.d.ts.map +1 -0
  184. package/dist/logger/transports/http.js +401 -0
  185. package/dist/logger/transports/http.js.map +1 -0
  186. package/dist/logger/transports/webhook.d.ts +152 -0
  187. package/dist/logger/transports/webhook.d.ts.map +1 -0
  188. package/dist/logger/transports/webhook.js +485 -0
  189. package/dist/logger/transports/webhook.js.map +1 -0
  190. package/dist/queue/defaults.d.ts +66 -0
  191. package/dist/queue/defaults.d.ts.map +1 -0
  192. package/dist/queue/defaults.js +205 -0
  193. package/dist/queue/defaults.js.map +1 -0
  194. package/dist/queue/index.d.ts +124 -0
  195. package/dist/queue/index.d.ts.map +1 -0
  196. package/dist/queue/index.js +116 -0
  197. package/dist/queue/index.js.map +1 -0
  198. package/dist/queue/queue.d.ts +156 -0
  199. package/dist/queue/queue.d.ts.map +1 -0
  200. package/dist/queue/queue.js +387 -0
  201. package/dist/queue/queue.js.map +1 -0
  202. package/dist/queue/transports/database.d.ts +165 -0
  203. package/dist/queue/transports/database.d.ts.map +1 -0
  204. package/dist/queue/transports/database.js +595 -0
  205. package/dist/queue/transports/database.js.map +1 -0
  206. package/dist/queue/transports/memory.d.ts +143 -0
  207. package/dist/queue/transports/memory.d.ts.map +1 -0
  208. package/dist/queue/transports/memory.js +415 -0
  209. package/dist/queue/transports/memory.js.map +1 -0
  210. package/dist/queue/transports/redis.d.ts +203 -0
  211. package/dist/queue/transports/redis.d.ts.map +1 -0
  212. package/dist/queue/transports/redis.js +744 -0
  213. package/dist/queue/transports/redis.js.map +1 -0
  214. package/dist/security/defaults.d.ts +64 -0
  215. package/dist/security/defaults.d.ts.map +1 -0
  216. package/dist/security/defaults.js +159 -0
  217. package/dist/security/defaults.js.map +1 -0
  218. package/dist/security/index.d.ts +110 -0
  219. package/dist/security/index.d.ts.map +1 -0
  220. package/dist/security/index.js +160 -0
  221. package/dist/security/index.js.map +1 -0
  222. package/dist/security/security.d.ts +138 -0
  223. package/dist/security/security.d.ts.map +1 -0
  224. package/dist/security/security.js +419 -0
  225. package/dist/security/security.js.map +1 -0
  226. package/dist/storage/defaults.d.ts +79 -0
  227. package/dist/storage/defaults.d.ts.map +1 -0
  228. package/dist/storage/defaults.js +358 -0
  229. package/dist/storage/defaults.js.map +1 -0
  230. package/dist/storage/index.d.ts +153 -0
  231. package/dist/storage/index.d.ts.map +1 -0
  232. package/dist/storage/index.js +242 -0
  233. package/dist/storage/index.js.map +1 -0
  234. package/dist/storage/storage.d.ts +151 -0
  235. package/dist/storage/storage.d.ts.map +1 -0
  236. package/dist/storage/storage.js +439 -0
  237. package/dist/storage/storage.js.map +1 -0
  238. package/dist/storage/strategies/local.d.ts +117 -0
  239. package/dist/storage/strategies/local.d.ts.map +1 -0
  240. package/dist/storage/strategies/local.js +368 -0
  241. package/dist/storage/strategies/local.js.map +1 -0
  242. package/dist/storage/strategies/r2.d.ts +130 -0
  243. package/dist/storage/strategies/r2.d.ts.map +1 -0
  244. package/dist/storage/strategies/r2.js +470 -0
  245. package/dist/storage/strategies/r2.js.map +1 -0
  246. package/dist/storage/strategies/s3.d.ts +121 -0
  247. package/dist/storage/strategies/s3.d.ts.map +1 -0
  248. package/dist/storage/strategies/s3.js +461 -0
  249. package/dist/storage/strategies/s3.js.map +1 -0
  250. package/dist/util/defaults.d.ts +77 -0
  251. package/dist/util/defaults.d.ts.map +1 -0
  252. package/dist/util/defaults.js +193 -0
  253. package/dist/util/defaults.js.map +1 -0
  254. package/dist/util/index.d.ts +97 -0
  255. package/dist/util/index.d.ts.map +1 -0
  256. package/dist/util/index.js +165 -0
  257. package/dist/util/index.js.map +1 -0
  258. package/dist/util/util.d.ts +145 -0
  259. package/dist/util/util.d.ts.map +1 -0
  260. package/dist/util/util.js +481 -0
  261. package/dist/util/util.js.map +1 -0
  262. 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