@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,819 @@
1
+ // @bun
2
+ var __defProp = Object.defineProperty;
3
+ var __export = (target, all) => {
4
+ for (var name in all)
5
+ __defProp(target, name, {
6
+ get: all[name],
7
+ enumerable: true,
8
+ configurable: true,
9
+ set: (newValue) => all[name] = () => newValue
10
+ });
11
+ };
12
+ var __legacyDecorateClassTS = function(decorators, target, key, desc) {
13
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
14
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function")
15
+ r = Reflect.decorate(decorators, target, key, desc);
16
+ else
17
+ for (var i = decorators.length - 1;i >= 0; i--)
18
+ if (d = decorators[i])
19
+ r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
20
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
21
+ };
22
+ var __legacyMetadataTS = (k, v) => {
23
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function")
24
+ return Reflect.metadata(k, v);
25
+ };
26
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
27
+ var __require = import.meta.require;
28
+
29
+ // src/jobs/drivers/memory.ts
30
+ class MemoryJobQueueDriver {
31
+ config;
32
+ jobStore = new Map;
33
+ pendingQueue = [];
34
+ processingSet = new Set;
35
+ failedQueue = [];
36
+ cleanupInterval = null;
37
+ jobCounter = 0;
38
+ metrics = {
39
+ enqueued: 0,
40
+ processed: 0,
41
+ failed: 0,
42
+ pending: 0,
43
+ processing: 0,
44
+ avgLatency: 0,
45
+ totalLatency: 0,
46
+ retried: 0,
47
+ successRate: 0,
48
+ avgAttempts: 0
49
+ };
50
+ constructor(config = {}) {
51
+ this.config = config;
52
+ this._startCleanup();
53
+ }
54
+ generateJobId() {
55
+ this.jobCounter++;
56
+ return `job:memory:${Date.now()}:${this.jobCounter}`;
57
+ }
58
+ _startCleanup() {
59
+ this.cleanupInterval = setInterval(() => {
60
+ const now = Date.now();
61
+ for (const [jobId, stored] of this.jobStore.entries()) {
62
+ if (now > stored.expiresAt) {
63
+ this.jobStore.delete(jobId);
64
+ }
65
+ }
66
+ }, 30000);
67
+ }
68
+ async connect() {}
69
+ async disconnect() {
70
+ if (this.cleanupInterval) {
71
+ clearInterval(this.cleanupInterval);
72
+ this.cleanupInterval = null;
73
+ }
74
+ this.jobStore.clear();
75
+ this.pendingQueue = [];
76
+ this.processingSet.clear();
77
+ this.failedQueue = [];
78
+ }
79
+ async isConnected() {
80
+ return true;
81
+ }
82
+ async enqueue(name, data, options) {
83
+ const jobId = this.generateJobId();
84
+ const now = new Date;
85
+ const job = {
86
+ id: jobId,
87
+ name,
88
+ data,
89
+ status: "pending",
90
+ attempts: 0,
91
+ maxRetries: options?.maxRetries ?? this.config.maxRetries ?? 3,
92
+ createdAt: now.toISOString(),
93
+ updatedAt: now.toISOString(),
94
+ priority: options?.priority ?? 0,
95
+ metadata: options?.metadata
96
+ };
97
+ if (options?.delay) {
98
+ const delayMs = options.delay instanceof Date ? options.delay.getTime() - Date.now() : options.delay;
99
+ if (delayMs > 0) {
100
+ job.status = "delayed";
101
+ job.scheduledFor = new Date(Date.now() + delayMs).toISOString();
102
+ }
103
+ }
104
+ const expiryMs = 7 * 24 * 60 * 60 * 1000;
105
+ this.jobStore.set(jobId, {
106
+ job,
107
+ expiresAt: Date.now() + expiryMs
108
+ });
109
+ if (job.status === "pending") {
110
+ this.pendingQueue.push(jobId);
111
+ }
112
+ this.metrics.enqueued++;
113
+ this.metrics.pending = this.pendingQueue.length;
114
+ return jobId;
115
+ }
116
+ async claim(count, timeout) {
117
+ const now = Date.now();
118
+ const claimed = [];
119
+ const toRemove = [];
120
+ this.jobStore.forEach((stored, jobId) => {
121
+ if (stored.job.status === "delayed" && stored.job.scheduledFor && new Date(stored.job.scheduledFor).getTime() <= now) {
122
+ stored.job.status = "pending";
123
+ if (!this.pendingQueue.includes(jobId)) {
124
+ this.pendingQueue.push(jobId);
125
+ }
126
+ }
127
+ });
128
+ while (claimed.length < count && this.pendingQueue.length > 0) {
129
+ const jobId = this.pendingQueue.shift();
130
+ if (!jobId)
131
+ break;
132
+ const stored = this.jobStore.get(jobId);
133
+ if (!stored)
134
+ continue;
135
+ const job = stored.job;
136
+ job.status = "processing";
137
+ job.attempts++;
138
+ job.startedAt = new Date().toISOString();
139
+ job.claimExpiresAt = new Date(now + timeout).toISOString();
140
+ job.updatedAt = new Date().toISOString();
141
+ this.processingSet.add(jobId);
142
+ claimed.push(job);
143
+ }
144
+ this.metrics.processing = this.processingSet.size;
145
+ this.metrics.pending = this.pendingQueue.length;
146
+ return claimed;
147
+ }
148
+ async complete(jobId) {
149
+ const stored = this.jobStore.get(jobId);
150
+ if (!stored)
151
+ return;
152
+ const job = stored.job;
153
+ const now = new Date;
154
+ job.status = "completed";
155
+ job.completedAt = now.toISOString();
156
+ job.updatedAt = now.toISOString();
157
+ if (job.startedAt) {
158
+ job.duration = new Date(job.completedAt).getTime() - new Date(job.startedAt).getTime();
159
+ this.metrics.totalLatency += job.duration;
160
+ }
161
+ this.processingSet.delete(jobId);
162
+ this.metrics.processed++;
163
+ this.metrics.processing = this.processingSet.size;
164
+ if (this.metrics.processed > 0) {
165
+ this.metrics.avgLatency = this.metrics.totalLatency / this.metrics.processed;
166
+ }
167
+ const total = this.metrics.processed + this.metrics.failed + this.metrics.pending;
168
+ if (total > 0) {
169
+ this.metrics.successRate = this.metrics.processed / total;
170
+ }
171
+ const allJobs = Array.from(this.jobStore.values()).map((s) => s.job).filter((j) => j.status === "completed" || j.status === "failed");
172
+ if (allJobs.length > 0) {
173
+ const totalAttempts = allJobs.reduce((sum, j) => sum + (j.attempts || 1), 0);
174
+ this.metrics.avgAttempts = totalAttempts / allJobs.length;
175
+ }
176
+ }
177
+ async fail(jobId, error, stackTrace) {
178
+ const stored = this.jobStore.get(jobId);
179
+ if (!stored)
180
+ return;
181
+ const job = stored.job;
182
+ const now = new Date;
183
+ job.error = error;
184
+ job.stackTrace = stackTrace;
185
+ job.status = "failed";
186
+ job.updatedAt = now.toISOString();
187
+ this.processingSet.delete(jobId);
188
+ this.failedQueue.push(jobId);
189
+ this.metrics.failed++;
190
+ this.metrics.processing = this.processingSet.size;
191
+ const total = this.metrics.processed + this.metrics.failed + this.metrics.pending;
192
+ if (total > 0) {
193
+ this.metrics.successRate = this.metrics.processed / total;
194
+ }
195
+ }
196
+ async scheduleRetry(jobId, delayMs, error) {
197
+ const stored = this.jobStore.get(jobId);
198
+ if (!stored)
199
+ return;
200
+ const job = stored.job;
201
+ const now = new Date;
202
+ job.error = error;
203
+ job.status = "delayed";
204
+ job.scheduledFor = new Date(now.getTime() + delayMs).toISOString();
205
+ job.updatedAt = now.toISOString();
206
+ this.processingSet.delete(jobId);
207
+ const failedIndex = this.failedQueue.indexOf(jobId);
208
+ if (failedIndex >= 0) {
209
+ this.failedQueue.splice(failedIndex, 1);
210
+ }
211
+ this.metrics.retried++;
212
+ this.metrics.processing = this.processingSet.size;
213
+ }
214
+ async getJob(jobId) {
215
+ const stored = this.jobStore.get(jobId);
216
+ return stored ? stored.job : null;
217
+ }
218
+ async getMetrics() {
219
+ return { ...this.metrics };
220
+ }
221
+ async clear() {
222
+ this.jobStore.clear();
223
+ this.pendingQueue = [];
224
+ this.processingSet.clear();
225
+ this.failedQueue = [];
226
+ this.metrics = {
227
+ enqueued: 0,
228
+ processed: 0,
229
+ failed: 0,
230
+ pending: 0,
231
+ processing: 0,
232
+ avgLatency: 0,
233
+ totalLatency: 0,
234
+ retried: 0,
235
+ successRate: 0,
236
+ avgAttempts: 0
237
+ };
238
+ }
239
+ }
240
+
241
+ // src/jobs/drivers/redis.ts
242
+ class RedisJobQueueDriver {
243
+ config;
244
+ client = null;
245
+ _isConnected = false;
246
+ keyPrefix;
247
+ constructor(config = {}) {
248
+ this.config = config;
249
+ this.keyPrefix = config.keyPrefix ?? "jobs:";
250
+ }
251
+ key(suffix) {
252
+ return `${this.keyPrefix}${suffix}`;
253
+ }
254
+ getClient() {
255
+ return this.client;
256
+ }
257
+ async connect() {
258
+ try {
259
+ const { RedisClient } = await Promise.resolve(globalThis.Bun);
260
+ if (!this.config.url) {
261
+ throw new Error("Redis URL is required for RedisJobQueueDriver");
262
+ }
263
+ this.client = new RedisClient(this.config.url);
264
+ this._isConnected = true;
265
+ } catch (error) {
266
+ throw new Error(`Failed to connect to Redis: ${error instanceof Error ? error.message : String(error)}`);
267
+ }
268
+ }
269
+ async disconnect() {
270
+ this._isConnected = false;
271
+ this.client = null;
272
+ }
273
+ async isConnected() {
274
+ return this._isConnected;
275
+ }
276
+ async enqueue(name, data, options) {
277
+ const client = this.getClient();
278
+ const jobId = `${name}:${Date.now()}:${Math.random().toString(36).slice(2)}`;
279
+ const now = new Date;
280
+ const job = {
281
+ id: jobId,
282
+ name,
283
+ data,
284
+ status: "pending",
285
+ attempts: 0,
286
+ maxRetries: options?.maxRetries ?? this.config.maxRetries ?? 3,
287
+ createdAt: now.toISOString(),
288
+ updatedAt: now.toISOString(),
289
+ priority: options?.priority ?? 0,
290
+ metadata: options?.metadata
291
+ };
292
+ let scheduledScore = now.getTime();
293
+ if (options?.delay) {
294
+ const delayMs = options.delay instanceof Date ? options.delay.getTime() - Date.now() : options.delay;
295
+ if (delayMs > 0) {
296
+ job.status = "delayed";
297
+ job.scheduledFor = new Date(now.getTime() + delayMs).toISOString();
298
+ scheduledScore = now.getTime() + delayMs;
299
+ }
300
+ }
301
+ await client.set(this.key(`job:${jobId}`), JSON.stringify(job));
302
+ await client.zadd(this.key("queue:pending"), scheduledScore, jobId);
303
+ await client.hincrby(this.key("metrics"), "enqueued", 1);
304
+ return jobId;
305
+ }
306
+ async claim(count, timeout) {
307
+ const client = this.getClient();
308
+ const now = Date.now();
309
+ const claimed = [];
310
+ try {
311
+ const jobIds = await client.zrange(this.key("queue:pending"), 0, count - 1);
312
+ for (const jobId of jobIds) {
313
+ if (claimed.length >= count)
314
+ break;
315
+ const jobKey = this.key(`job:${jobId}`);
316
+ const jobData = await client.get(jobKey);
317
+ if (!jobData)
318
+ continue;
319
+ const job = JSON.parse(jobData);
320
+ job.status = "processing";
321
+ job.attempts = (job.attempts || 0) + 1;
322
+ job.startedAt = new Date().toISOString();
323
+ job.claimExpiresAt = new Date(now + timeout).toISOString();
324
+ job.updatedAt = new Date().toISOString();
325
+ await client.set(jobKey, JSON.stringify(job));
326
+ await client.zadd(this.key("queue:processing"), now + timeout, jobId);
327
+ await client.zrem(this.key("queue:pending"), jobId);
328
+ claimed.push(job);
329
+ }
330
+ return claimed;
331
+ } catch (error) {
332
+ console.error("Error claiming jobs:", error);
333
+ return [];
334
+ }
335
+ }
336
+ async complete(jobId) {
337
+ const client = this.getClient();
338
+ const jobKey = this.key(`job:${jobId}`);
339
+ const jobData = await client.get(jobKey);
340
+ if (!jobData)
341
+ return;
342
+ const job = JSON.parse(jobData);
343
+ const now = new Date;
344
+ job.status = "completed";
345
+ job.completedAt = now.toISOString();
346
+ job.updatedAt = now.toISOString();
347
+ if (job.startedAt) {
348
+ job.duration = new Date(job.completedAt).getTime() - new Date(job.startedAt).getTime();
349
+ }
350
+ await client.set(jobKey, JSON.stringify(job));
351
+ await client.zrem(this.key("queue:processing"), jobId);
352
+ await client.hincrby(this.key("metrics"), "processed", 1);
353
+ if (job.duration) {
354
+ await client.hincrby(this.key("metrics"), "totalLatency", Math.floor(job.duration));
355
+ }
356
+ }
357
+ async fail(jobId, error, stackTrace) {
358
+ const client = this.getClient();
359
+ const jobKey = this.key(`job:${jobId}`);
360
+ const jobData = await client.get(jobKey);
361
+ if (!jobData)
362
+ return;
363
+ const job = JSON.parse(jobData);
364
+ job.error = error;
365
+ job.stackTrace = stackTrace;
366
+ job.status = "failed";
367
+ job.updatedAt = new Date().toISOString();
368
+ await client.set(jobKey, JSON.stringify(job));
369
+ await client.zrem(this.key("queue:processing"), jobId);
370
+ await client.zadd(this.key("queue:failed"), Date.now(), jobId);
371
+ await client.hincrby(this.key("metrics"), "failed", 1);
372
+ }
373
+ async scheduleRetry(jobId, delayMs, error) {
374
+ const client = this.getClient();
375
+ const jobKey = this.key(`job:${jobId}`);
376
+ const jobData = await client.get(jobKey);
377
+ if (!jobData)
378
+ return;
379
+ const job = JSON.parse(jobData);
380
+ job.status = "delayed";
381
+ job.error = error;
382
+ job.scheduledFor = new Date(Date.now() + delayMs).toISOString();
383
+ job.updatedAt = new Date().toISOString();
384
+ await client.set(jobKey, JSON.stringify(job));
385
+ await client.zrem(this.key("queue:processing"), jobId);
386
+ await client.zadd(this.key("queue:pending"), Date.now() + delayMs, jobId);
387
+ await client.hincrby(this.key("metrics"), "retried", 1);
388
+ }
389
+ async getJob(jobId) {
390
+ const client = this.getClient();
391
+ const jobData = await client.get(this.key(`job:${jobId}`));
392
+ if (!jobData)
393
+ return null;
394
+ return JSON.parse(jobData);
395
+ }
396
+ async getMetrics() {
397
+ const client = this.getClient();
398
+ const metricsData = await client.hgetall(this.key("metrics"));
399
+ const pendingCount = await client.zcard(this.key("queue:pending"));
400
+ const processingCount = await client.zcard(this.key("queue:processing"));
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
+ const total = enqueued;
407
+ const successRate = total > 0 ? processed / total : 0;
408
+ const avgLatency = processed > 0 ? totalLatency / processed : 0;
409
+ return {
410
+ enqueued,
411
+ processed,
412
+ failed,
413
+ pending: pendingCount,
414
+ processing: processingCount,
415
+ avgLatency,
416
+ totalLatency,
417
+ retried,
418
+ successRate,
419
+ avgAttempts: enqueued > 0 ? (processed + failed) / enqueued : 0
420
+ };
421
+ }
422
+ async clear() {
423
+ const client = this.getClient();
424
+ const keys = [
425
+ this.key("queue:pending"),
426
+ this.key("queue:processing"),
427
+ this.key("queue:failed"),
428
+ this.key("metrics")
429
+ ];
430
+ const pendingJobs = await client.zrange(this.key("queue:pending"), 0, -1);
431
+ for (const jobId of pendingJobs) {
432
+ keys.push(this.key(`job:${jobId}`));
433
+ }
434
+ const processingJobs = await client.zrange(this.key("queue:processing"), 0, -1);
435
+ for (const jobId of processingJobs) {
436
+ keys.push(this.key(`job:${jobId}`));
437
+ }
438
+ const failedJobs = await client.zrange(this.key("queue:failed"), 0, -1);
439
+ for (const jobId of failedJobs) {
440
+ keys.push(this.key(`job:${jobId}`));
441
+ }
442
+ if (keys.length > 0) {
443
+ await client.del(...keys);
444
+ }
445
+ }
446
+ }
447
+
448
+ // src/jobs/queue.ts
449
+ class JobQueue {
450
+ driver;
451
+ handlers = new Map;
452
+ handlerRegistry = [];
453
+ eventListeners = new Map;
454
+ isRunning = false;
455
+ constructor(config = {}) {
456
+ const driver = config.driver ?? "memory";
457
+ if (driver === "redis") {
458
+ if (!config.url) {
459
+ throw new Error("Redis URL is required for Redis driver");
460
+ }
461
+ this.driver = new RedisJobQueueDriver(config);
462
+ } else {
463
+ this.driver = new MemoryJobQueueDriver(config);
464
+ }
465
+ }
466
+ async init() {
467
+ await this.driver.connect();
468
+ }
469
+ async shutdown() {
470
+ this.isRunning = false;
471
+ await this.driver.disconnect();
472
+ }
473
+ async enqueue(name, data, options) {
474
+ const jobId = await this.driver.enqueue(name, data, options);
475
+ this._emitEvent("enqueued", { id: jobId, name });
476
+ return jobId;
477
+ }
478
+ on(pattern, handler) {
479
+ this.handlers.set(pattern, handler);
480
+ const specificity = pattern.split(".").length;
481
+ const entry = { pattern, handler, specificity };
482
+ const index = this.handlerRegistry.findIndex((e) => e.specificity < specificity);
483
+ if (index >= 0) {
484
+ this.handlerRegistry.splice(index, 0, entry);
485
+ } else {
486
+ this.handlerRegistry.push(entry);
487
+ }
488
+ }
489
+ off(pattern) {
490
+ this.handlers.delete(pattern);
491
+ const index = this.handlerRegistry.findIndex((e) => e.pattern === pattern);
492
+ if (index >= 0) {
493
+ this.handlerRegistry.splice(index, 1);
494
+ }
495
+ }
496
+ findHandler(jobName) {
497
+ for (const entry of this.handlerRegistry) {
498
+ if (this._patternMatches(entry.pattern, jobName)) {
499
+ return entry.handler;
500
+ }
501
+ }
502
+ return null;
503
+ }
504
+ _patternMatches(pattern, jobName) {
505
+ if (pattern === jobName)
506
+ return true;
507
+ if (pattern.endsWith(".*")) {
508
+ const prefix = pattern.slice(0, -2);
509
+ return jobName.startsWith(prefix + ".");
510
+ }
511
+ return false;
512
+ }
513
+ onEvent(event, listener) {
514
+ if (!this.eventListeners.has(event)) {
515
+ this.eventListeners.set(event, new Set);
516
+ }
517
+ this.eventListeners.get(event).add(listener);
518
+ }
519
+ offEvent(event, listener) {
520
+ this.eventListeners.get(event)?.delete(listener);
521
+ }
522
+ _emitEvent(eventType, job) {
523
+ const listeners = this.eventListeners.get(eventType);
524
+ if (!listeners)
525
+ return;
526
+ const event = {
527
+ type: eventType,
528
+ job,
529
+ timestamp: new Date
530
+ };
531
+ for (const listener of listeners) {
532
+ try {
533
+ listener(event);
534
+ } catch (error) {
535
+ console.error(`Error in ${eventType} listener:`, error);
536
+ }
537
+ }
538
+ }
539
+ async start(options = {}) {
540
+ this.isRunning = true;
541
+ const pollInterval = options.pollInterval ?? 1000;
542
+ const concurrency = options.concurrency ?? 10;
543
+ while (this.isRunning) {
544
+ try {
545
+ const jobs = await this.driver.claim(concurrency, 30000);
546
+ for (const job of jobs) {
547
+ if (!this.isRunning)
548
+ break;
549
+ try {
550
+ const handler = this.findHandler(job.name);
551
+ if (!handler) {
552
+ console.warn(`No handler found for job type: ${job.name}`);
553
+ await this.driver.complete(job.id);
554
+ continue;
555
+ }
556
+ this._emitEvent("started", job);
557
+ await handler(job);
558
+ this._emitEvent("completed", job);
559
+ await this.driver.complete(job.id);
560
+ } catch (error) {
561
+ const errorMsg = error instanceof Error ? error.message : String(error);
562
+ const stackTrace = error instanceof Error ? error.stack : undefined;
563
+ if (job.attempts < job.maxRetries) {
564
+ const backoffMs = Math.pow(2, job.attempts) * 1000;
565
+ this._emitEvent("retried", job);
566
+ await this.driver.scheduleRetry(job.id, backoffMs, errorMsg);
567
+ } else {
568
+ this._emitEvent("failed", job);
569
+ await this.driver.fail(job.id, errorMsg, stackTrace);
570
+ }
571
+ }
572
+ }
573
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
574
+ } catch (error) {
575
+ console.error("Error in job queue polling loop:", error);
576
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
577
+ }
578
+ }
579
+ }
580
+ async stop() {
581
+ this.isRunning = false;
582
+ await new Promise((resolve) => setTimeout(resolve, 1000));
583
+ }
584
+ async getJob(jobId) {
585
+ return this.driver.getJob(jobId);
586
+ }
587
+ async getMetrics() {
588
+ return this.driver.getMetrics();
589
+ }
590
+ async clear() {
591
+ return this.driver.clear();
592
+ }
593
+ async isConnected() {
594
+ return this.driver.isConnected();
595
+ }
596
+ }
597
+ function createJobQueue(config) {
598
+ return new JobQueue(config);
599
+ }
600
+ // src/jobs/worker.ts
601
+ class JobWorker {
602
+ driver;
603
+ handlers = new Map;
604
+ handlerRegistry = [];
605
+ eventListeners = new Map;
606
+ isRunning = false;
607
+ pollInterval;
608
+ concurrency;
609
+ jobTimeout;
610
+ maxBackoffDelay = 30000;
611
+ currentBackoff = 0;
612
+ inFlightJobs = new Set;
613
+ shutdownTimeout = 1e4;
614
+ constructor(config = {}) {
615
+ const driver = config.driver ?? "memory";
616
+ if (driver === "redis") {
617
+ if (!config.url) {
618
+ throw new Error("Redis URL is required for Redis driver");
619
+ }
620
+ this.driver = new RedisJobQueueDriver(config);
621
+ } else {
622
+ this.driver = new MemoryJobQueueDriver(config);
623
+ }
624
+ this.pollInterval = config.pollInterval ?? 1000;
625
+ this.concurrency = config.concurrency ?? 10;
626
+ this.jobTimeout = config.jobTimeout ?? 300000;
627
+ }
628
+ async init() {
629
+ await this.driver.connect();
630
+ }
631
+ handle(pattern, handler) {
632
+ this.handlers.set(pattern, handler);
633
+ const specificity = pattern.split(".").length;
634
+ const entry = { pattern, handler, specificity };
635
+ const index = this.handlerRegistry.findIndex((e) => e.specificity < specificity);
636
+ if (index >= 0) {
637
+ this.handlerRegistry.splice(index, 0, entry);
638
+ } else {
639
+ this.handlerRegistry.push(entry);
640
+ }
641
+ }
642
+ unhandle(pattern) {
643
+ this.handlers.delete(pattern);
644
+ const index = this.handlerRegistry.findIndex((e) => e.pattern === pattern);
645
+ if (index >= 0) {
646
+ this.handlerRegistry.splice(index, 1);
647
+ }
648
+ }
649
+ on(eventType, listener) {
650
+ if (!this.eventListeners.has(eventType)) {
651
+ this.eventListeners.set(eventType, new Set);
652
+ }
653
+ this.eventListeners.get(eventType).add(listener);
654
+ }
655
+ off(eventType, listener) {
656
+ this.eventListeners.get(eventType)?.delete(listener);
657
+ }
658
+ _emitEvent(eventType, job) {
659
+ const listeners = this.eventListeners.get(eventType);
660
+ if (!listeners)
661
+ return;
662
+ const event = {
663
+ type: eventType,
664
+ job,
665
+ timestamp: new Date
666
+ };
667
+ for (const listener of listeners) {
668
+ try {
669
+ listener(event);
670
+ } catch (error) {
671
+ console.error(`Error in ${eventType} listener:`, error);
672
+ }
673
+ }
674
+ }
675
+ findHandler(jobName) {
676
+ for (const entry of this.handlerRegistry) {
677
+ if (this._patternMatches(entry.pattern, jobName)) {
678
+ return entry.handler;
679
+ }
680
+ }
681
+ return null;
682
+ }
683
+ _patternMatches(pattern, jobName) {
684
+ if (pattern === jobName)
685
+ return true;
686
+ if (pattern.endsWith(".*")) {
687
+ const prefix = pattern.slice(0, -2);
688
+ return jobName.startsWith(prefix + ".");
689
+ }
690
+ return false;
691
+ }
692
+ async start() {
693
+ this.isRunning = true;
694
+ console.log("[JobWorker] Starting worker process");
695
+ const handleSignal = async () => {
696
+ await this.stop();
697
+ };
698
+ process.on("SIGTERM", handleSignal);
699
+ process.on("SIGINT", handleSignal);
700
+ try {
701
+ await this._pollLoop();
702
+ } finally {
703
+ process.removeListener("SIGTERM", handleSignal);
704
+ process.removeListener("SIGINT", handleSignal);
705
+ }
706
+ }
707
+ async _pollLoop() {
708
+ while (this.isRunning) {
709
+ try {
710
+ const availableSlots = this.concurrency - this.inFlightJobs.size;
711
+ if (availableSlots > 0) {
712
+ const jobs = await this.driver.claim(availableSlots, this.jobTimeout);
713
+ if (jobs.length > 0) {
714
+ this.currentBackoff = 0;
715
+ for (const job of jobs) {
716
+ this._processJob(job).catch((error) => {
717
+ console.error(`[JobWorker] Unhandled error processing job ${job.id}:`, error);
718
+ });
719
+ }
720
+ } else {
721
+ this.currentBackoff = Math.min((this.currentBackoff || this.pollInterval) * 1.5, this.maxBackoffDelay);
722
+ }
723
+ }
724
+ const delay = this.currentBackoff || this.pollInterval;
725
+ await new Promise((resolve) => setTimeout(resolve, delay));
726
+ } catch (error) {
727
+ console.error("[JobWorker] Error in polling loop:", error);
728
+ this.currentBackoff = Math.min((this.currentBackoff || this.pollInterval) * 1.5, this.maxBackoffDelay);
729
+ await new Promise((resolve) => setTimeout(resolve, this.currentBackoff));
730
+ }
731
+ }
732
+ }
733
+ async _processJob(job) {
734
+ this.inFlightJobs.add(job.id);
735
+ try {
736
+ const handler = this.findHandler(job.name);
737
+ if (!handler) {
738
+ console.warn(`[JobWorker] No handler found for job type: ${job.name}`);
739
+ await this.driver.complete(job.id);
740
+ return;
741
+ }
742
+ this._emitEvent("started", job);
743
+ const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error(`Job timeout after ${this.jobTimeout}ms`)), this.jobTimeout));
744
+ const handlerPromise = handler(job);
745
+ await Promise.race([handlerPromise, timeoutPromise]);
746
+ this._emitEvent("completed", job);
747
+ await this.driver.complete(job.id);
748
+ } catch (error) {
749
+ const errorMsg = error instanceof Error ? error.message : String(error);
750
+ const stackTrace = error instanceof Error ? error.stack : undefined;
751
+ if (job.attempts < job.maxRetries) {
752
+ const delaySeconds = Math.min(Math.pow(2, job.attempts), 3600);
753
+ const delayMs = delaySeconds * 1000;
754
+ this._emitEvent("retried", job);
755
+ await this.driver.scheduleRetry(job.id, delayMs, errorMsg);
756
+ console.warn(`[JobWorker] Job ${job.id} failed (attempt ${job.attempts}/${job.maxRetries}): ${errorMsg}. Retrying in ${delaySeconds}s`);
757
+ } else {
758
+ this._emitEvent("failed", job);
759
+ await this.driver.fail(job.id, errorMsg, stackTrace);
760
+ console.error(`[JobWorker] Job ${job.id} failed permanently: ${errorMsg}`);
761
+ }
762
+ } finally {
763
+ this.inFlightJobs.delete(job.id);
764
+ }
765
+ }
766
+ async stop() {
767
+ if (!this.isRunning)
768
+ return;
769
+ console.log("[JobWorker] Shutting down gracefully...");
770
+ this.isRunning = false;
771
+ const startTime = Date.now();
772
+ while (this.inFlightJobs.size > 0 && Date.now() - startTime < this.shutdownTimeout) {
773
+ await new Promise((resolve) => setTimeout(resolve, 100));
774
+ }
775
+ if (this.inFlightJobs.size > 0) {
776
+ console.warn(`[JobWorker] Force shutdown with ${this.inFlightJobs.size} jobs still in flight`);
777
+ } else {
778
+ console.log("[JobWorker] All jobs completed, shutting down");
779
+ }
780
+ await this.driver.disconnect();
781
+ }
782
+ getInFlightCount() {
783
+ return this.inFlightJobs.size;
784
+ }
785
+ async getMetrics() {
786
+ return this.driver.getMetrics();
787
+ }
788
+ isActive() {
789
+ return this.isRunning;
790
+ }
791
+ }
792
+ async function startWorker(config) {
793
+ const worker = new JobWorker(config);
794
+ const metricsInterval = setInterval(async () => {
795
+ if (worker.isActive()) {
796
+ const metrics = await worker.getMetrics();
797
+ console.log("[JobWorker] Metrics:", {
798
+ pending: metrics.pending,
799
+ processing: metrics.processing,
800
+ processed: metrics.processed,
801
+ failed: metrics.failed,
802
+ avgLatency: `${Math.round(metrics.avgLatency)}ms`
803
+ });
804
+ }
805
+ }, 30000);
806
+ await worker.init();
807
+ process.on("exit", () => {
808
+ clearInterval(metricsInterval);
809
+ });
810
+ await worker.start();
811
+ }
812
+ export {
813
+ startWorker,
814
+ createJobQueue,
815
+ RedisJobQueueDriver,
816
+ MemoryJobQueueDriver,
817
+ JobWorker,
818
+ JobQueue
819
+ };