@buenojs/bueno 0.8.4 → 0.8.6

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 (234) hide show
  1. package/README.md +264 -17
  2. package/dist/cli/{index.js → bin.js} +413 -332
  3. package/dist/container/index.js +273 -0
  4. package/dist/context/index.js +219 -0
  5. package/dist/database/index.js +493 -0
  6. package/dist/frontend/index.js +7697 -0
  7. package/dist/graphql/index.js +2156 -0
  8. package/dist/health/index.js +364 -0
  9. package/dist/i18n/index.js +345 -0
  10. package/dist/index.js +9694 -5047
  11. package/dist/jobs/index.js +819 -0
  12. package/dist/lock/index.js +367 -0
  13. package/dist/logger/index.js +281 -0
  14. package/dist/metrics/index.js +289 -0
  15. package/dist/middleware/index.js +77 -0
  16. package/dist/migrations/index.js +571 -0
  17. package/dist/modules/index.js +3411 -0
  18. package/dist/notification/index.js +484 -0
  19. package/dist/observability/index.js +331 -0
  20. package/dist/openapi/index.js +795 -0
  21. package/dist/orm/index.js +1356 -0
  22. package/dist/router/index.js +886 -0
  23. package/dist/rpc/index.js +691 -0
  24. package/dist/schema/index.js +400 -0
  25. package/dist/telemetry/index.js +595 -0
  26. package/dist/template/index.js +640 -0
  27. package/dist/templates/index.js +640 -0
  28. package/dist/testing/index.js +1111 -0
  29. package/dist/types/index.js +60 -0
  30. package/llms.txt +231 -0
  31. package/package.json +125 -27
  32. package/src/cache/index.ts +2 -1
  33. package/src/cli/ARCHITECTURE.md +3 -3
  34. package/src/cli/bin.ts +2 -2
  35. package/src/cli/commands/build.ts +183 -165
  36. package/src/cli/commands/dev.ts +96 -89
  37. package/src/cli/commands/generate.ts +142 -111
  38. package/src/cli/commands/help.ts +20 -16
  39. package/src/cli/commands/index.ts +3 -6
  40. package/src/cli/commands/migration.ts +124 -105
  41. package/src/cli/commands/new.ts +294 -232
  42. package/src/cli/commands/start.ts +81 -79
  43. package/src/cli/core/args.ts +68 -50
  44. package/src/cli/core/console.ts +89 -95
  45. package/src/cli/core/index.ts +4 -4
  46. package/src/cli/core/prompt.ts +65 -62
  47. package/src/cli/core/spinner.ts +23 -20
  48. package/src/cli/index.ts +46 -38
  49. package/src/cli/templates/database/index.ts +37 -18
  50. package/src/cli/templates/database/mysql.ts +3 -3
  51. package/src/cli/templates/database/none.ts +2 -2
  52. package/src/cli/templates/database/postgresql.ts +3 -3
  53. package/src/cli/templates/database/sqlite.ts +3 -3
  54. package/src/cli/templates/deploy.ts +29 -26
  55. package/src/cli/templates/docker.ts +41 -30
  56. package/src/cli/templates/frontend/index.ts +33 -15
  57. package/src/cli/templates/frontend/none.ts +2 -2
  58. package/src/cli/templates/frontend/react.ts +18 -18
  59. package/src/cli/templates/frontend/solid.ts +15 -15
  60. package/src/cli/templates/frontend/svelte.ts +17 -17
  61. package/src/cli/templates/frontend/vue.ts +15 -15
  62. package/src/cli/templates/generators/index.ts +29 -29
  63. package/src/cli/templates/generators/types.ts +21 -21
  64. package/src/cli/templates/index.ts +6 -6
  65. package/src/cli/templates/project/api.ts +37 -36
  66. package/src/cli/templates/project/default.ts +25 -25
  67. package/src/cli/templates/project/fullstack.ts +28 -26
  68. package/src/cli/templates/project/index.ts +55 -16
  69. package/src/cli/templates/project/minimal.ts +17 -12
  70. package/src/cli/templates/project/types.ts +10 -5
  71. package/src/cli/templates/project/website.ts +15 -15
  72. package/src/cli/utils/fs.ts +55 -41
  73. package/src/cli/utils/index.ts +3 -3
  74. package/src/cli/utils/strings.ts +47 -33
  75. package/src/cli/utils/version.ts +14 -8
  76. package/src/config/env-validation.ts +100 -0
  77. package/src/config/env.ts +169 -41
  78. package/src/config/index.ts +28 -20
  79. package/src/config/loader.ts +25 -16
  80. package/src/config/merge.ts +21 -10
  81. package/src/config/types.ts +566 -25
  82. package/src/config/validation.ts +215 -7
  83. package/src/container/forward-ref.ts +22 -22
  84. package/src/container/index.ts +34 -12
  85. package/src/context/index.ts +11 -1
  86. package/src/database/index.ts +7 -190
  87. package/src/database/orm/builder.ts +457 -0
  88. package/src/database/orm/casts/index.ts +130 -0
  89. package/src/database/orm/casts/types.ts +25 -0
  90. package/src/database/orm/compiler.ts +304 -0
  91. package/src/database/orm/hooks/index.ts +114 -0
  92. package/src/database/orm/index.ts +61 -0
  93. package/src/database/orm/model-registry.ts +59 -0
  94. package/src/database/orm/model.ts +821 -0
  95. package/src/database/orm/relationships/base.ts +146 -0
  96. package/src/database/orm/relationships/belongs-to-many.ts +179 -0
  97. package/src/database/orm/relationships/belongs-to.ts +56 -0
  98. package/src/database/orm/relationships/has-many.ts +45 -0
  99. package/src/database/orm/relationships/has-one.ts +41 -0
  100. package/src/database/orm/relationships/index.ts +11 -0
  101. package/src/database/orm/scopes/index.ts +55 -0
  102. package/src/events/__tests__/event-system.test.ts +235 -0
  103. package/src/events/config.ts +238 -0
  104. package/src/events/example-usage.ts +185 -0
  105. package/src/events/index.ts +278 -0
  106. package/src/events/manager.ts +385 -0
  107. package/src/events/registry.ts +182 -0
  108. package/src/events/types.ts +124 -0
  109. package/src/frontend/api-routes.ts +65 -23
  110. package/src/frontend/bundler.ts +76 -34
  111. package/src/frontend/console-client.ts +2 -2
  112. package/src/frontend/console-stream.ts +94 -38
  113. package/src/frontend/dev-server.ts +94 -46
  114. package/src/frontend/file-router.ts +61 -19
  115. package/src/frontend/frameworks/index.ts +37 -10
  116. package/src/frontend/frameworks/react.ts +10 -8
  117. package/src/frontend/frameworks/solid.ts +11 -9
  118. package/src/frontend/frameworks/svelte.ts +15 -9
  119. package/src/frontend/frameworks/vue.ts +13 -11
  120. package/src/frontend/hmr-client.ts +12 -10
  121. package/src/frontend/hmr.ts +146 -103
  122. package/src/frontend/index.ts +14 -5
  123. package/src/frontend/islands.ts +41 -22
  124. package/src/frontend/isr.ts +59 -37
  125. package/src/frontend/layout.ts +36 -21
  126. package/src/frontend/ssr/react.ts +74 -27
  127. package/src/frontend/ssr/solid.ts +54 -20
  128. package/src/frontend/ssr/svelte.ts +48 -14
  129. package/src/frontend/ssr/vue.ts +50 -18
  130. package/src/frontend/ssr.ts +83 -39
  131. package/src/frontend/types.ts +91 -56
  132. package/src/graphql/built-in-engine.ts +598 -0
  133. package/src/graphql/context-builder.ts +110 -0
  134. package/src/graphql/decorators.ts +358 -0
  135. package/src/graphql/execution-pipeline.ts +227 -0
  136. package/src/graphql/graphql-module.ts +563 -0
  137. package/src/graphql/index.ts +101 -0
  138. package/src/graphql/metadata.ts +237 -0
  139. package/src/graphql/schema-builder.ts +319 -0
  140. package/src/graphql/subscription-handler.ts +283 -0
  141. package/src/graphql/types.ts +324 -0
  142. package/src/health/index.ts +21 -9
  143. package/src/i18n/engine.ts +305 -0
  144. package/src/i18n/index.ts +38 -0
  145. package/src/i18n/loader.ts +218 -0
  146. package/src/i18n/middleware.ts +164 -0
  147. package/src/i18n/negotiator.ts +162 -0
  148. package/src/i18n/types.ts +158 -0
  149. package/src/index.ts +182 -27
  150. package/src/jobs/drivers/memory.ts +315 -0
  151. package/src/jobs/drivers/redis.ts +459 -0
  152. package/src/jobs/index.ts +30 -0
  153. package/src/jobs/queue.ts +281 -0
  154. package/src/jobs/types.ts +295 -0
  155. package/src/jobs/worker.ts +380 -0
  156. package/src/logger/index.ts +1 -3
  157. package/src/logger/transports/index.ts +62 -22
  158. package/src/metrics/index.ts +25 -16
  159. package/src/migrations/index.ts +9 -0
  160. package/src/modules/filters.ts +13 -17
  161. package/src/modules/guards.ts +49 -26
  162. package/src/modules/index.ts +457 -299
  163. package/src/modules/interceptors.ts +58 -20
  164. package/src/modules/lazy.ts +11 -19
  165. package/src/modules/lifecycle.ts +15 -7
  166. package/src/modules/metadata.ts +15 -5
  167. package/src/modules/pipes.ts +94 -72
  168. package/src/notification/channels/base.ts +68 -0
  169. package/src/notification/channels/email.ts +105 -0
  170. package/src/notification/channels/push.ts +104 -0
  171. package/src/notification/channels/sms.ts +105 -0
  172. package/src/notification/channels/whatsapp.ts +104 -0
  173. package/src/notification/index.ts +48 -0
  174. package/src/notification/service.ts +354 -0
  175. package/src/notification/types.ts +344 -0
  176. package/src/observability/__tests__/observability.test.ts +483 -0
  177. package/src/observability/breadcrumbs.ts +114 -0
  178. package/src/observability/index.ts +136 -0
  179. package/src/observability/interceptor.ts +85 -0
  180. package/src/observability/service.ts +303 -0
  181. package/src/observability/trace.ts +37 -0
  182. package/src/observability/types.ts +196 -0
  183. package/src/openapi/__tests__/decorators.test.ts +335 -0
  184. package/src/openapi/__tests__/document-builder.test.ts +285 -0
  185. package/src/openapi/__tests__/route-scanner.test.ts +334 -0
  186. package/src/openapi/__tests__/schema-generator.test.ts +275 -0
  187. package/src/openapi/decorators.ts +328 -0
  188. package/src/openapi/document-builder.ts +274 -0
  189. package/src/openapi/index.ts +112 -0
  190. package/src/openapi/metadata.ts +112 -0
  191. package/src/openapi/route-scanner.ts +289 -0
  192. package/src/openapi/schema-generator.ts +256 -0
  193. package/src/openapi/swagger-module.ts +166 -0
  194. package/src/openapi/types.ts +398 -0
  195. package/src/orm/index.ts +10 -0
  196. package/src/rpc/index.ts +3 -1
  197. package/src/schema/index.ts +9 -0
  198. package/src/security/index.ts +15 -6
  199. package/src/ssg/index.ts +9 -8
  200. package/src/telemetry/index.ts +76 -22
  201. package/src/template/index.ts +7 -0
  202. package/src/templates/engine.ts +224 -0
  203. package/src/templates/index.ts +9 -0
  204. package/src/templates/loader.ts +331 -0
  205. package/src/templates/renderers/markdown.ts +212 -0
  206. package/src/templates/renderers/simple.ts +269 -0
  207. package/src/templates/types.ts +154 -0
  208. package/src/testing/index.ts +100 -27
  209. package/src/types/optional-deps.d.ts +347 -187
  210. package/src/validation/index.ts +92 -2
  211. package/src/validation/schemas.ts +536 -0
  212. package/tests/integration/cli.test.ts +19 -19
  213. package/tests/integration/fullstack.test.ts +4 -4
  214. package/tests/unit/cli.test.ts +1 -1
  215. package/tests/unit/database.test.ts +2 -72
  216. package/tests/unit/env-validation.test.ts +166 -0
  217. package/tests/unit/events.test.ts +910 -0
  218. package/tests/unit/graphql.test.ts +991 -0
  219. package/tests/unit/i18n.test.ts +455 -0
  220. package/tests/unit/jobs.test.ts +493 -0
  221. package/tests/unit/notification.test.ts +988 -0
  222. package/tests/unit/observability.test.ts +453 -0
  223. package/tests/unit/orm/builder.test.ts +323 -0
  224. package/tests/unit/orm/casts.test.ts +179 -0
  225. package/tests/unit/orm/compiler.test.ts +220 -0
  226. package/tests/unit/orm/eager-loading.test.ts +285 -0
  227. package/tests/unit/orm/hooks.test.ts +191 -0
  228. package/tests/unit/orm/model.test.ts +373 -0
  229. package/tests/unit/orm/relationships.test.ts +303 -0
  230. package/tests/unit/orm/scopes.test.ts +74 -0
  231. package/tests/unit/templates-simple.test.ts +53 -0
  232. package/tests/unit/templates.test.ts +454 -0
  233. package/tests/unit/validation.test.ts +18 -24
  234. package/tsconfig.json +11 -3
@@ -0,0 +1,459 @@
1
+ /**
2
+ * Redis Job Queue Driver
3
+ *
4
+ * Production-grade driver using Bun's native Redis client.
5
+ * Supports atomic operations via Lua scripts.
6
+ */
7
+
8
+ import type {
9
+ Job,
10
+ JobQueueConfig,
11
+ JobQueueDriver,
12
+ QueueMetrics,
13
+ QueueOptions,
14
+ } from "../types";
15
+
16
+ // ============= Lua Scripts =============
17
+
18
+ /**
19
+ * Atomic job claim script
20
+ * Moves a job from pending to processing with ownership verification
21
+ */
22
+ const CLAIM_JOB_SCRIPT = `
23
+ local jobId = KEYS[1]
24
+ local processingKey = KEYS[2]
25
+ local jobKey = KEYS[3]
26
+
27
+ local jobData = redis.call("ZRANGE", processingKey, 0, 0)[1]
28
+ if jobData then
29
+ return nil
30
+ end
31
+
32
+ -- Get the job from pending
33
+ local job = redis.call("GET", jobKey)
34
+ if not job then
35
+ return nil
36
+ end
37
+
38
+ -- Move to processing with expiry timestamp
39
+ local expiryMs = tonumber(ARGV[1])
40
+ redis.call("ZADD", processingKey, expiryMs, jobId)
41
+ redis.call("ZREM", ARGV[2], jobId)
42
+
43
+ return job
44
+ `;
45
+
46
+ /**
47
+ * Atomic job completion script
48
+ */
49
+ const COMPLETE_JOB_SCRIPT = `
50
+ local jobId = KEYS[1]
51
+ local jobKey = KEYS[2]
52
+ local processingKey = KEYS[3]
53
+
54
+ local job = redis.call("GET", jobKey)
55
+ if not job then
56
+ return nil
57
+ end
58
+
59
+ -- Update job status in JSON
60
+ local decoded = cjson.decode(job)
61
+ decoded.status = "completed"
62
+ decoded.completedAt = ARGV[1]
63
+ decoded.updatedAt = ARGV[1]
64
+
65
+ if decoded.startedAt then
66
+ local started = tonumber(string.sub(decoded.startedAt, 1, -5))
67
+ local ended = tonumber(string.sub(ARGV[1], 1, -5))
68
+ decoded.duration = ended - started
69
+ end
70
+
71
+ local updated = cjson.encode(decoded)
72
+ redis.call("SET", jobKey, updated)
73
+ redis.call("ZREM", processingKey, jobId)
74
+
75
+ return updated
76
+ `;
77
+
78
+ /**
79
+ * Atomic job failure script
80
+ */
81
+ const FAIL_JOB_SCRIPT = `
82
+ local jobId = KEYS[1]
83
+ local jobKey = KEYS[2]
84
+ local processingKey = KEYS[3]
85
+ local failedKey = KEYS[4]
86
+
87
+ local job = redis.call("GET", jobKey)
88
+ if not job then
89
+ return nil
90
+ end
91
+
92
+ local decoded = cjson.decode(job)
93
+ decoded.status = "failed"
94
+ decoded.error = ARGV[1]
95
+ if ARGV[2] and ARGV[2] ~= "" then
96
+ decoded.stackTrace = ARGV[2]
97
+ end
98
+ decoded.updatedAt = ARGV[3]
99
+
100
+ local updated = cjson.encode(decoded)
101
+ redis.call("SET", jobKey, updated)
102
+ redis.call("ZREM", processingKey, jobId)
103
+ redis.call("ZADD", failedKey, tonumber(ARGV[3]), jobId)
104
+
105
+ return updated
106
+ `;
107
+
108
+ /**
109
+ * Schedule retry script
110
+ */
111
+ const SCHEDULE_RETRY_SCRIPT = `
112
+ local jobId = KEYS[1]
113
+ local jobKey = KEYS[2]
114
+ local processingKey = KEYS[3]
115
+ local failedKey = KEYS[4]
116
+ local pendingKey = KEYS[5]
117
+
118
+ local job = redis.call("GET", jobKey)
119
+ if not job then
120
+ return nil
121
+ end
122
+
123
+ local decoded = cjson.decode(job)
124
+ decoded.status = "delayed"
125
+ decoded.error = ARGV[1]
126
+ decoded.scheduledFor = ARGV[2]
127
+ decoded.updatedAt = ARGV[3]
128
+
129
+ local updated = cjson.encode(decoded)
130
+ redis.call("SET", jobKey, updated)
131
+ redis.call("ZREM", processingKey, jobId)
132
+ redis.call("ZREM", failedKey, jobId)
133
+ -- Add to pending with scheduled timestamp
134
+ redis.call("ZADD", pendingKey, tonumber(ARGV[4]), jobId)
135
+
136
+ return updated
137
+ `;
138
+
139
+ // ============= Redis Job Queue Driver =============
140
+
141
+ export class RedisJobQueueDriver implements JobQueueDriver {
142
+ private client: unknown = null;
143
+ private _isConnected = false;
144
+ private keyPrefix: string;
145
+
146
+ constructor(private config: JobQueueConfig = {}) {
147
+ this.keyPrefix = config.keyPrefix ?? "jobs:";
148
+ }
149
+
150
+ /**
151
+ * Get prefixed key name
152
+ */
153
+ private key(suffix: string): string {
154
+ return `${this.keyPrefix}${suffix}`;
155
+ }
156
+
157
+ /**
158
+ * Type-safe Redis client getter
159
+ */
160
+ private getClient(): {
161
+ get: (key: string) => Promise<string | null>;
162
+ set: (key: string, value: string) => Promise<void>;
163
+ zadd: (
164
+ key: string,
165
+ score: number | string,
166
+ member: string,
167
+ ) => Promise<number>;
168
+ zrange: (
169
+ key: string,
170
+ start: number,
171
+ stop: number,
172
+ options?: { withscores?: boolean },
173
+ ) => Promise<string[]>;
174
+ zrem: (key: string, ...members: string[]) => Promise<number>;
175
+ zcard: (key: string) => Promise<number>;
176
+ del: (...keys: string[]) => Promise<number>;
177
+ hgetall: (key: string) => Promise<Record<string, string>>;
178
+ hset: (key: string, field: string, value: string) => Promise<number>;
179
+ hincrby: (key: string, field: string, increment: number) => Promise<number>;
180
+ eval: (script: string, keys: string[], args: string[]) => Promise<unknown>;
181
+ flushdb: () => Promise<void>;
182
+ } {
183
+ return this.client as any;
184
+ }
185
+
186
+ async connect(): Promise<void> {
187
+ try {
188
+ // @ts-ignore - Bun runtime API
189
+ const { RedisClient } = await import("bun");
190
+ if (!this.config.url) {
191
+ throw new Error("Redis URL is required for RedisJobQueueDriver");
192
+ }
193
+ this.client = new RedisClient(this.config.url);
194
+ this._isConnected = true;
195
+ } catch (error) {
196
+ throw new Error(
197
+ `Failed to connect to Redis: ${error instanceof Error ? error.message : String(error)}`,
198
+ );
199
+ }
200
+ }
201
+
202
+ async disconnect(): Promise<void> {
203
+ this._isConnected = false;
204
+ this.client = null;
205
+ }
206
+
207
+ async isConnected(): Promise<boolean> {
208
+ return this._isConnected;
209
+ }
210
+
211
+ async enqueue<T = unknown>(
212
+ name: string,
213
+ data: T,
214
+ options?: QueueOptions,
215
+ ): Promise<string> {
216
+ const client = this.getClient();
217
+ const jobId = `${name}:${Date.now()}:${Math.random().toString(36).slice(2)}`;
218
+ const now = new Date();
219
+
220
+ const job: Job<T> = {
221
+ id: jobId,
222
+ name,
223
+ data,
224
+ status: "pending",
225
+ attempts: 0,
226
+ maxRetries: options?.maxRetries ?? this.config.maxRetries ?? 3,
227
+ createdAt: now.toISOString(),
228
+ updatedAt: now.toISOString(),
229
+ priority: options?.priority ?? 0,
230
+ metadata: options?.metadata,
231
+ };
232
+
233
+ // Handle delayed jobs
234
+ let scheduledScore = now.getTime();
235
+ if (options?.delay) {
236
+ const delayMs =
237
+ options.delay instanceof Date
238
+ ? options.delay.getTime() - Date.now()
239
+ : options.delay;
240
+
241
+ if (delayMs > 0) {
242
+ job.status = "delayed";
243
+ job.scheduledFor = new Date(now.getTime() + delayMs).toISOString();
244
+ scheduledScore = now.getTime() + delayMs;
245
+ }
246
+ }
247
+
248
+ // Store job data
249
+ await client.set(this.key(`job:${jobId}`), JSON.stringify(job));
250
+
251
+ // Add to pending queue (sorted by creation time for FIFO)
252
+ await client.zadd(this.key("queue:pending"), scheduledScore, jobId);
253
+
254
+ // Update metrics
255
+ await client.hincrby(this.key("metrics"), "enqueued", 1);
256
+
257
+ return jobId;
258
+ }
259
+
260
+ async claim(count: number, timeout: number): Promise<Job[]> {
261
+ const client = this.getClient();
262
+ const now = Date.now();
263
+ const claimed: Job[] = [];
264
+
265
+ try {
266
+ // Get pending jobs (oldest first - FIFO)
267
+ const jobIds = await client.zrange(
268
+ this.key("queue:pending"),
269
+ 0,
270
+ count - 1,
271
+ );
272
+
273
+ for (const jobId of jobIds) {
274
+ if (claimed.length >= count) break;
275
+
276
+ const jobKey = this.key(`job:${jobId}`);
277
+ const jobData = await client.get(jobKey);
278
+
279
+ if (!jobData) continue;
280
+
281
+ const job: Job = JSON.parse(jobData);
282
+ job.status = "processing";
283
+ job.attempts = (job.attempts || 0) + 1;
284
+ job.startedAt = new Date().toISOString();
285
+ job.claimExpiresAt = new Date(now + timeout).toISOString();
286
+ job.updatedAt = new Date().toISOString();
287
+
288
+ // Atomically move to processing
289
+ await client.set(jobKey, JSON.stringify(job));
290
+ await client.zadd(this.key("queue:processing"), now + timeout, jobId);
291
+ await client.zrem(this.key("queue:pending"), jobId);
292
+
293
+ claimed.push(job);
294
+ }
295
+
296
+ return claimed;
297
+ } catch (error) {
298
+ console.error("Error claiming jobs:", error);
299
+ return [];
300
+ }
301
+ }
302
+
303
+ async complete(jobId: string): Promise<void> {
304
+ const client = this.getClient();
305
+ const jobKey = this.key(`job:${jobId}`);
306
+ const jobData = await client.get(jobKey);
307
+
308
+ if (!jobData) return;
309
+
310
+ const job: Job = JSON.parse(jobData);
311
+ const now = new Date();
312
+
313
+ job.status = "completed";
314
+ job.completedAt = now.toISOString();
315
+ job.updatedAt = now.toISOString();
316
+
317
+ if (job.startedAt) {
318
+ job.duration =
319
+ new Date(job.completedAt).getTime() - new Date(job.startedAt).getTime();
320
+ }
321
+
322
+ // Update job and remove from processing
323
+ await client.set(jobKey, JSON.stringify(job));
324
+ await client.zrem(this.key("queue:processing"), jobId);
325
+
326
+ // Update metrics
327
+ await client.hincrby(this.key("metrics"), "processed", 1);
328
+ if (job.duration) {
329
+ await client.hincrby(
330
+ this.key("metrics"),
331
+ "totalLatency",
332
+ Math.floor(job.duration),
333
+ );
334
+ }
335
+ }
336
+
337
+ async fail(jobId: string, error: string, stackTrace?: string): Promise<void> {
338
+ const client = this.getClient();
339
+ const jobKey = this.key(`job:${jobId}`);
340
+ const jobData = await client.get(jobKey);
341
+
342
+ if (!jobData) return;
343
+
344
+ const job: Job = JSON.parse(jobData);
345
+ job.error = error;
346
+ job.stackTrace = stackTrace;
347
+ job.status = "failed";
348
+ job.updatedAt = new Date().toISOString();
349
+
350
+ await client.set(jobKey, JSON.stringify(job));
351
+ await client.zrem(this.key("queue:processing"), jobId);
352
+ await client.zadd(this.key("queue:failed"), Date.now(), jobId);
353
+
354
+ // Update metrics
355
+ await client.hincrby(this.key("metrics"), "failed", 1);
356
+ }
357
+
358
+ async scheduleRetry(
359
+ jobId: string,
360
+ delayMs: number,
361
+ error: string,
362
+ ): Promise<void> {
363
+ const client = this.getClient();
364
+ const jobKey = this.key(`job:${jobId}`);
365
+ const jobData = await client.get(jobKey);
366
+
367
+ if (!jobData) return;
368
+
369
+ const job: Job = JSON.parse(jobData);
370
+ job.status = "delayed";
371
+ job.error = error;
372
+ job.scheduledFor = new Date(Date.now() + delayMs).toISOString();
373
+ job.updatedAt = new Date().toISOString();
374
+
375
+ await client.set(jobKey, JSON.stringify(job));
376
+ await client.zrem(this.key("queue:processing"), jobId);
377
+
378
+ // Re-add to pending with scheduled timestamp
379
+ await client.zadd(this.key("queue:pending"), Date.now() + delayMs, jobId);
380
+
381
+ // Update metrics
382
+ await client.hincrby(this.key("metrics"), "retried", 1);
383
+ }
384
+
385
+ async getJob(jobId: string): Promise<Job | null> {
386
+ const client = this.getClient();
387
+ const jobData = await client.get(this.key(`job:${jobId}`));
388
+
389
+ if (!jobData) return null;
390
+
391
+ return JSON.parse(jobData) as Job;
392
+ }
393
+
394
+ async getMetrics(): Promise<QueueMetrics> {
395
+ const client = this.getClient();
396
+
397
+ const metricsData = await client.hgetall(this.key("metrics"));
398
+ const pendingCount = await client.zcard(this.key("queue:pending"));
399
+ const processingCount = await client.zcard(this.key("queue:processing"));
400
+
401
+ const enqueued = Number.parseInt(metricsData.enqueued || "0");
402
+ const processed = Number.parseInt(metricsData.processed || "0");
403
+ const failed = Number.parseInt(metricsData.failed || "0");
404
+ const totalLatency = Number.parseInt(metricsData.totalLatency || "0");
405
+ const retried = Number.parseInt(metricsData.retried || "0");
406
+
407
+ const total = enqueued;
408
+ const successRate = total > 0 ? processed / total : 0;
409
+ const avgLatency = processed > 0 ? totalLatency / processed : 0;
410
+
411
+ return {
412
+ enqueued,
413
+ processed,
414
+ failed,
415
+ pending: pendingCount,
416
+ processing: processingCount,
417
+ avgLatency,
418
+ totalLatency,
419
+ retried,
420
+ successRate,
421
+ avgAttempts: enqueued > 0 ? (processed + failed) / enqueued : 0,
422
+ };
423
+ }
424
+
425
+ async clear(): Promise<void> {
426
+ const client = this.getClient();
427
+
428
+ const keys = [
429
+ this.key("queue:pending"),
430
+ this.key("queue:processing"),
431
+ this.key("queue:failed"),
432
+ this.key("metrics"),
433
+ ];
434
+
435
+ // Also clear all job data
436
+ const pendingJobs = await client.zrange(this.key("queue:pending"), 0, -1);
437
+ for (const jobId of pendingJobs) {
438
+ keys.push(this.key(`job:${jobId}`));
439
+ }
440
+
441
+ const processingJobs = await client.zrange(
442
+ this.key("queue:processing"),
443
+ 0,
444
+ -1,
445
+ );
446
+ for (const jobId of processingJobs) {
447
+ keys.push(this.key(`job:${jobId}`));
448
+ }
449
+
450
+ const failedJobs = await client.zrange(this.key("queue:failed"), 0, -1);
451
+ for (const jobId of failedJobs) {
452
+ keys.push(this.key(`job:${jobId}`));
453
+ }
454
+
455
+ if (keys.length > 0) {
456
+ await client.del(...keys);
457
+ }
458
+ }
459
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Background Jobs Module
3
+ *
4
+ * Complete job queue system with Redis/Memory drivers,
5
+ * handler registration, and event system.
6
+ */
7
+
8
+ // Export types
9
+ export type {
10
+ Job,
11
+ JobStatus,
12
+ JobQueueConfig,
13
+ QueueOptions,
14
+ JobHandler,
15
+ QueueMetrics,
16
+ JobEvent,
17
+ JobEventType,
18
+ JobClaimHandle,
19
+ JobQueueDriver,
20
+ HandlerRegistryEntry,
21
+ JobConfigValidationResult,
22
+ } from "./types";
23
+
24
+ // Export main classes
25
+ export { JobQueue, createJobQueue } from "./queue";
26
+ export { JobWorker, startWorker } from "./worker";
27
+
28
+ // Export drivers
29
+ export { MemoryJobQueueDriver } from "./drivers/memory";
30
+ export { RedisJobQueueDriver } from "./drivers/redis";