@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.
- package/README.md +264 -17
- package/dist/cli/{index.js → bin.js} +413 -332
- package/dist/container/index.js +273 -0
- package/dist/context/index.js +219 -0
- package/dist/database/index.js +493 -0
- package/dist/frontend/index.js +7697 -0
- package/dist/graphql/index.js +2156 -0
- package/dist/health/index.js +364 -0
- package/dist/i18n/index.js +345 -0
- package/dist/index.js +9694 -5047
- package/dist/jobs/index.js +819 -0
- package/dist/lock/index.js +367 -0
- package/dist/logger/index.js +281 -0
- package/dist/metrics/index.js +289 -0
- package/dist/middleware/index.js +77 -0
- package/dist/migrations/index.js +571 -0
- package/dist/modules/index.js +3411 -0
- package/dist/notification/index.js +484 -0
- package/dist/observability/index.js +331 -0
- package/dist/openapi/index.js +795 -0
- package/dist/orm/index.js +1356 -0
- package/dist/router/index.js +886 -0
- package/dist/rpc/index.js +691 -0
- package/dist/schema/index.js +400 -0
- package/dist/telemetry/index.js +595 -0
- package/dist/template/index.js +640 -0
- package/dist/templates/index.js +640 -0
- package/dist/testing/index.js +1111 -0
- package/dist/types/index.js +60 -0
- package/llms.txt +231 -0
- package/package.json +125 -27
- package/src/cache/index.ts +2 -1
- package/src/cli/ARCHITECTURE.md +3 -3
- package/src/cli/bin.ts +2 -2
- package/src/cli/commands/build.ts +183 -165
- package/src/cli/commands/dev.ts +96 -89
- package/src/cli/commands/generate.ts +142 -111
- package/src/cli/commands/help.ts +20 -16
- package/src/cli/commands/index.ts +3 -6
- package/src/cli/commands/migration.ts +124 -105
- package/src/cli/commands/new.ts +294 -232
- package/src/cli/commands/start.ts +81 -79
- package/src/cli/core/args.ts +68 -50
- package/src/cli/core/console.ts +89 -95
- package/src/cli/core/index.ts +4 -4
- package/src/cli/core/prompt.ts +65 -62
- package/src/cli/core/spinner.ts +23 -20
- package/src/cli/index.ts +46 -38
- package/src/cli/templates/database/index.ts +37 -18
- package/src/cli/templates/database/mysql.ts +3 -3
- package/src/cli/templates/database/none.ts +2 -2
- package/src/cli/templates/database/postgresql.ts +3 -3
- package/src/cli/templates/database/sqlite.ts +3 -3
- package/src/cli/templates/deploy.ts +29 -26
- package/src/cli/templates/docker.ts +41 -30
- package/src/cli/templates/frontend/index.ts +33 -15
- package/src/cli/templates/frontend/none.ts +2 -2
- package/src/cli/templates/frontend/react.ts +18 -18
- package/src/cli/templates/frontend/solid.ts +15 -15
- package/src/cli/templates/frontend/svelte.ts +17 -17
- package/src/cli/templates/frontend/vue.ts +15 -15
- package/src/cli/templates/generators/index.ts +29 -29
- package/src/cli/templates/generators/types.ts +21 -21
- package/src/cli/templates/index.ts +6 -6
- package/src/cli/templates/project/api.ts +37 -36
- package/src/cli/templates/project/default.ts +25 -25
- package/src/cli/templates/project/fullstack.ts +28 -26
- package/src/cli/templates/project/index.ts +55 -16
- package/src/cli/templates/project/minimal.ts +17 -12
- package/src/cli/templates/project/types.ts +10 -5
- package/src/cli/templates/project/website.ts +15 -15
- package/src/cli/utils/fs.ts +55 -41
- package/src/cli/utils/index.ts +3 -3
- package/src/cli/utils/strings.ts +47 -33
- package/src/cli/utils/version.ts +14 -8
- package/src/config/env-validation.ts +100 -0
- package/src/config/env.ts +169 -41
- package/src/config/index.ts +28 -20
- package/src/config/loader.ts +25 -16
- package/src/config/merge.ts +21 -10
- package/src/config/types.ts +566 -25
- package/src/config/validation.ts +215 -7
- package/src/container/forward-ref.ts +22 -22
- package/src/container/index.ts +34 -12
- package/src/context/index.ts +11 -1
- package/src/database/index.ts +7 -190
- package/src/database/orm/builder.ts +457 -0
- package/src/database/orm/casts/index.ts +130 -0
- package/src/database/orm/casts/types.ts +25 -0
- package/src/database/orm/compiler.ts +304 -0
- package/src/database/orm/hooks/index.ts +114 -0
- package/src/database/orm/index.ts +61 -0
- package/src/database/orm/model-registry.ts +59 -0
- package/src/database/orm/model.ts +821 -0
- package/src/database/orm/relationships/base.ts +146 -0
- package/src/database/orm/relationships/belongs-to-many.ts +179 -0
- package/src/database/orm/relationships/belongs-to.ts +56 -0
- package/src/database/orm/relationships/has-many.ts +45 -0
- package/src/database/orm/relationships/has-one.ts +41 -0
- package/src/database/orm/relationships/index.ts +11 -0
- package/src/database/orm/scopes/index.ts +55 -0
- package/src/events/__tests__/event-system.test.ts +235 -0
- package/src/events/config.ts +238 -0
- package/src/events/example-usage.ts +185 -0
- package/src/events/index.ts +278 -0
- package/src/events/manager.ts +385 -0
- package/src/events/registry.ts +182 -0
- package/src/events/types.ts +124 -0
- package/src/frontend/api-routes.ts +65 -23
- package/src/frontend/bundler.ts +76 -34
- package/src/frontend/console-client.ts +2 -2
- package/src/frontend/console-stream.ts +94 -38
- package/src/frontend/dev-server.ts +94 -46
- package/src/frontend/file-router.ts +61 -19
- package/src/frontend/frameworks/index.ts +37 -10
- package/src/frontend/frameworks/react.ts +10 -8
- package/src/frontend/frameworks/solid.ts +11 -9
- package/src/frontend/frameworks/svelte.ts +15 -9
- package/src/frontend/frameworks/vue.ts +13 -11
- package/src/frontend/hmr-client.ts +12 -10
- package/src/frontend/hmr.ts +146 -103
- package/src/frontend/index.ts +14 -5
- package/src/frontend/islands.ts +41 -22
- package/src/frontend/isr.ts +59 -37
- package/src/frontend/layout.ts +36 -21
- package/src/frontend/ssr/react.ts +74 -27
- package/src/frontend/ssr/solid.ts +54 -20
- package/src/frontend/ssr/svelte.ts +48 -14
- package/src/frontend/ssr/vue.ts +50 -18
- package/src/frontend/ssr.ts +83 -39
- package/src/frontend/types.ts +91 -56
- package/src/graphql/built-in-engine.ts +598 -0
- package/src/graphql/context-builder.ts +110 -0
- package/src/graphql/decorators.ts +358 -0
- package/src/graphql/execution-pipeline.ts +227 -0
- package/src/graphql/graphql-module.ts +563 -0
- package/src/graphql/index.ts +101 -0
- package/src/graphql/metadata.ts +237 -0
- package/src/graphql/schema-builder.ts +319 -0
- package/src/graphql/subscription-handler.ts +283 -0
- package/src/graphql/types.ts +324 -0
- package/src/health/index.ts +21 -9
- package/src/i18n/engine.ts +305 -0
- package/src/i18n/index.ts +38 -0
- package/src/i18n/loader.ts +218 -0
- package/src/i18n/middleware.ts +164 -0
- package/src/i18n/negotiator.ts +162 -0
- package/src/i18n/types.ts +158 -0
- package/src/index.ts +182 -27
- package/src/jobs/drivers/memory.ts +315 -0
- package/src/jobs/drivers/redis.ts +459 -0
- package/src/jobs/index.ts +30 -0
- package/src/jobs/queue.ts +281 -0
- package/src/jobs/types.ts +295 -0
- package/src/jobs/worker.ts +380 -0
- package/src/logger/index.ts +1 -3
- package/src/logger/transports/index.ts +62 -22
- package/src/metrics/index.ts +25 -16
- package/src/migrations/index.ts +9 -0
- package/src/modules/filters.ts +13 -17
- package/src/modules/guards.ts +49 -26
- package/src/modules/index.ts +457 -299
- package/src/modules/interceptors.ts +58 -20
- package/src/modules/lazy.ts +11 -19
- package/src/modules/lifecycle.ts +15 -7
- package/src/modules/metadata.ts +15 -5
- package/src/modules/pipes.ts +94 -72
- package/src/notification/channels/base.ts +68 -0
- package/src/notification/channels/email.ts +105 -0
- package/src/notification/channels/push.ts +104 -0
- package/src/notification/channels/sms.ts +105 -0
- package/src/notification/channels/whatsapp.ts +104 -0
- package/src/notification/index.ts +48 -0
- package/src/notification/service.ts +354 -0
- package/src/notification/types.ts +344 -0
- package/src/observability/__tests__/observability.test.ts +483 -0
- package/src/observability/breadcrumbs.ts +114 -0
- package/src/observability/index.ts +136 -0
- package/src/observability/interceptor.ts +85 -0
- package/src/observability/service.ts +303 -0
- package/src/observability/trace.ts +37 -0
- package/src/observability/types.ts +196 -0
- package/src/openapi/__tests__/decorators.test.ts +335 -0
- package/src/openapi/__tests__/document-builder.test.ts +285 -0
- package/src/openapi/__tests__/route-scanner.test.ts +334 -0
- package/src/openapi/__tests__/schema-generator.test.ts +275 -0
- package/src/openapi/decorators.ts +328 -0
- package/src/openapi/document-builder.ts +274 -0
- package/src/openapi/index.ts +112 -0
- package/src/openapi/metadata.ts +112 -0
- package/src/openapi/route-scanner.ts +289 -0
- package/src/openapi/schema-generator.ts +256 -0
- package/src/openapi/swagger-module.ts +166 -0
- package/src/openapi/types.ts +398 -0
- package/src/orm/index.ts +10 -0
- package/src/rpc/index.ts +3 -1
- package/src/schema/index.ts +9 -0
- package/src/security/index.ts +15 -6
- package/src/ssg/index.ts +9 -8
- package/src/telemetry/index.ts +76 -22
- package/src/template/index.ts +7 -0
- package/src/templates/engine.ts +224 -0
- package/src/templates/index.ts +9 -0
- package/src/templates/loader.ts +331 -0
- package/src/templates/renderers/markdown.ts +212 -0
- package/src/templates/renderers/simple.ts +269 -0
- package/src/templates/types.ts +154 -0
- package/src/testing/index.ts +100 -27
- package/src/types/optional-deps.d.ts +347 -187
- package/src/validation/index.ts +92 -2
- package/src/validation/schemas.ts +536 -0
- package/tests/integration/cli.test.ts +19 -19
- package/tests/integration/fullstack.test.ts +4 -4
- package/tests/unit/cli.test.ts +1 -1
- package/tests/unit/database.test.ts +2 -72
- package/tests/unit/env-validation.test.ts +166 -0
- package/tests/unit/events.test.ts +910 -0
- package/tests/unit/graphql.test.ts +991 -0
- package/tests/unit/i18n.test.ts +455 -0
- package/tests/unit/jobs.test.ts +493 -0
- package/tests/unit/notification.test.ts +988 -0
- package/tests/unit/observability.test.ts +453 -0
- package/tests/unit/orm/builder.test.ts +323 -0
- package/tests/unit/orm/casts.test.ts +179 -0
- package/tests/unit/orm/compiler.test.ts +220 -0
- package/tests/unit/orm/eager-loading.test.ts +285 -0
- package/tests/unit/orm/hooks.test.ts +191 -0
- package/tests/unit/orm/model.test.ts +373 -0
- package/tests/unit/orm/relationships.test.ts +303 -0
- package/tests/unit/orm/scopes.test.ts +74 -0
- package/tests/unit/templates-simple.test.ts +53 -0
- package/tests/unit/templates.test.ts +454 -0
- package/tests/unit/validation.test.ts +18 -24
- package/tsconfig.json +11 -3
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Job Worker
|
|
3
|
+
*
|
|
4
|
+
* Production-grade worker for processing background jobs.
|
|
5
|
+
* Designed to run in a separate process/worker.
|
|
6
|
+
* Supports graceful shutdown and exponential backoff polling.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { MemoryJobQueueDriver } from "./drivers/memory";
|
|
10
|
+
import { RedisJobQueueDriver } from "./drivers/redis";
|
|
11
|
+
import type {
|
|
12
|
+
HandlerRegistryEntry,
|
|
13
|
+
Job,
|
|
14
|
+
JobEvent,
|
|
15
|
+
JobEventType,
|
|
16
|
+
JobHandler,
|
|
17
|
+
JobQueueConfig,
|
|
18
|
+
JobQueueDriver,
|
|
19
|
+
} from "./types";
|
|
20
|
+
|
|
21
|
+
// ============= Job Worker Class =============
|
|
22
|
+
|
|
23
|
+
export class JobWorker {
|
|
24
|
+
private driver: JobQueueDriver;
|
|
25
|
+
private handlers: Map<string, JobHandler<unknown>> = new Map();
|
|
26
|
+
private handlerRegistry: HandlerRegistryEntry[] = [];
|
|
27
|
+
private eventListeners: Map<JobEventType, Set<(event: JobEvent) => void>> =
|
|
28
|
+
new Map();
|
|
29
|
+
private isRunning = false;
|
|
30
|
+
private pollInterval: number;
|
|
31
|
+
private concurrency: number;
|
|
32
|
+
private jobTimeout: number;
|
|
33
|
+
private maxBackoffDelay = 30000; // 30 seconds max backoff
|
|
34
|
+
private currentBackoff = 0;
|
|
35
|
+
private inFlightJobs = new Set<string>();
|
|
36
|
+
private shutdownTimeout = 10000; // 10 seconds to drain
|
|
37
|
+
|
|
38
|
+
constructor(config: JobQueueConfig = {}) {
|
|
39
|
+
// Instantiate appropriate driver
|
|
40
|
+
const driver = config.driver ?? "memory";
|
|
41
|
+
|
|
42
|
+
if (driver === "redis") {
|
|
43
|
+
if (!config.url) {
|
|
44
|
+
throw new Error("Redis URL is required for Redis driver");
|
|
45
|
+
}
|
|
46
|
+
this.driver = new RedisJobQueueDriver(config);
|
|
47
|
+
} else {
|
|
48
|
+
this.driver = new MemoryJobQueueDriver(config);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
this.pollInterval = config.pollInterval ?? 1000;
|
|
52
|
+
this.concurrency = config.concurrency ?? 10;
|
|
53
|
+
this.jobTimeout = config.jobTimeout ?? 300000; // 5 minutes default
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Initialize the worker (connect to backend)
|
|
58
|
+
*/
|
|
59
|
+
async init(): Promise<void> {
|
|
60
|
+
await this.driver.connect();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Register a handler for a job type
|
|
65
|
+
* Supports wildcards: "email.*" matches "email.welcome", "email.reset", etc.
|
|
66
|
+
*/
|
|
67
|
+
handle(pattern: string, handler: JobHandler<unknown>): void {
|
|
68
|
+
this.handlers.set(pattern, handler);
|
|
69
|
+
|
|
70
|
+
// Calculate specificity for wildcard matching (longer = more specific)
|
|
71
|
+
const specificity = pattern.split(".").length;
|
|
72
|
+
const entry: HandlerRegistryEntry = { pattern, handler, specificity };
|
|
73
|
+
|
|
74
|
+
// Insert in order of specificity (highest first)
|
|
75
|
+
const index = this.handlerRegistry.findIndex(
|
|
76
|
+
(e) => e.specificity < specificity,
|
|
77
|
+
);
|
|
78
|
+
if (index >= 0) {
|
|
79
|
+
this.handlerRegistry.splice(index, 0, entry);
|
|
80
|
+
} else {
|
|
81
|
+
this.handlerRegistry.push(entry);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Remove a handler
|
|
87
|
+
*/
|
|
88
|
+
unhandle(pattern: string): void {
|
|
89
|
+
this.handlers.delete(pattern);
|
|
90
|
+
const index = this.handlerRegistry.findIndex((e) => e.pattern === pattern);
|
|
91
|
+
if (index >= 0) {
|
|
92
|
+
this.handlerRegistry.splice(index, 1);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Listen for worker events
|
|
98
|
+
*/
|
|
99
|
+
on(eventType: JobEventType, listener: (event: JobEvent) => void): void {
|
|
100
|
+
if (!this.eventListeners.has(eventType)) {
|
|
101
|
+
this.eventListeners.set(eventType, new Set());
|
|
102
|
+
}
|
|
103
|
+
this.eventListeners.get(eventType)!.add(listener);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Stop listening for events
|
|
108
|
+
*/
|
|
109
|
+
off(eventType: JobEventType, listener: (event: JobEvent) => void): void {
|
|
110
|
+
this.eventListeners.get(eventType)?.delete(listener);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Emit a worker event
|
|
115
|
+
*/
|
|
116
|
+
private _emitEvent(eventType: JobEventType, job: Job): void {
|
|
117
|
+
const listeners = this.eventListeners.get(eventType);
|
|
118
|
+
if (!listeners) return;
|
|
119
|
+
|
|
120
|
+
const event: JobEvent = {
|
|
121
|
+
type: eventType,
|
|
122
|
+
job,
|
|
123
|
+
timestamp: new Date(),
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
for (const listener of listeners) {
|
|
127
|
+
try {
|
|
128
|
+
listener(event);
|
|
129
|
+
} catch (error) {
|
|
130
|
+
console.error(`Error in ${eventType} listener:`, error);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Find the best matching handler for a job
|
|
137
|
+
*/
|
|
138
|
+
private findHandler(jobName: string): JobHandler<unknown> | null {
|
|
139
|
+
for (const entry of this.handlerRegistry) {
|
|
140
|
+
if (this._patternMatches(entry.pattern, jobName)) {
|
|
141
|
+
return entry.handler;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Check if a pattern matches a job name (supports wildcards)
|
|
149
|
+
*/
|
|
150
|
+
private _patternMatches(pattern: string, jobName: string): boolean {
|
|
151
|
+
if (pattern === jobName) return true;
|
|
152
|
+
|
|
153
|
+
// Handle wildcards: "email.*" matches "email.welcome"
|
|
154
|
+
if (pattern.endsWith(".*")) {
|
|
155
|
+
const prefix = pattern.slice(0, -2); // Remove ".*"
|
|
156
|
+
return jobName.startsWith(prefix + ".");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Start the worker (blocks until stopped)
|
|
164
|
+
*/
|
|
165
|
+
async start(): Promise<void> {
|
|
166
|
+
this.isRunning = true;
|
|
167
|
+
|
|
168
|
+
console.log("[JobWorker] Starting worker process");
|
|
169
|
+
|
|
170
|
+
// Handle signals for graceful shutdown
|
|
171
|
+
const handleSignal = async () => {
|
|
172
|
+
await this.stop();
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
process.on("SIGTERM", handleSignal);
|
|
176
|
+
process.on("SIGINT", handleSignal);
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
await this._pollLoop();
|
|
180
|
+
} finally {
|
|
181
|
+
process.removeListener("SIGTERM", handleSignal);
|
|
182
|
+
process.removeListener("SIGINT", handleSignal);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Main polling loop
|
|
188
|
+
*/
|
|
189
|
+
private async _pollLoop(): Promise<void> {
|
|
190
|
+
while (this.isRunning) {
|
|
191
|
+
try {
|
|
192
|
+
const availableSlots = this.concurrency - this.inFlightJobs.size;
|
|
193
|
+
|
|
194
|
+
if (availableSlots > 0) {
|
|
195
|
+
const jobs = await this.driver.claim(availableSlots, this.jobTimeout);
|
|
196
|
+
|
|
197
|
+
if (jobs.length > 0) {
|
|
198
|
+
// Reset backoff on successful claim
|
|
199
|
+
this.currentBackoff = 0;
|
|
200
|
+
|
|
201
|
+
// Process jobs concurrently
|
|
202
|
+
for (const job of jobs) {
|
|
203
|
+
this._processJob(job).catch((error) => {
|
|
204
|
+
console.error(
|
|
205
|
+
`[JobWorker] Unhandled error processing job ${job.id}:`,
|
|
206
|
+
error,
|
|
207
|
+
);
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
} else {
|
|
211
|
+
// No jobs, increase backoff
|
|
212
|
+
this.currentBackoff = Math.min(
|
|
213
|
+
(this.currentBackoff || this.pollInterval) * 1.5,
|
|
214
|
+
this.maxBackoffDelay,
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Sleep before next poll
|
|
220
|
+
const delay = this.currentBackoff || this.pollInterval;
|
|
221
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
222
|
+
} catch (error) {
|
|
223
|
+
console.error("[JobWorker] Error in polling loop:", error);
|
|
224
|
+
|
|
225
|
+
// Backoff on error
|
|
226
|
+
this.currentBackoff = Math.min(
|
|
227
|
+
(this.currentBackoff || this.pollInterval) * 1.5,
|
|
228
|
+
this.maxBackoffDelay,
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
await new Promise((resolve) =>
|
|
232
|
+
setTimeout(resolve, this.currentBackoff),
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Process a single job
|
|
240
|
+
*/
|
|
241
|
+
private async _processJob(job: Job): Promise<void> {
|
|
242
|
+
this.inFlightJobs.add(job.id);
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
const handler = this.findHandler(job.name);
|
|
246
|
+
|
|
247
|
+
if (!handler) {
|
|
248
|
+
console.warn(`[JobWorker] No handler found for job type: ${job.name}`);
|
|
249
|
+
await this.driver.complete(job.id);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
this._emitEvent("started", job);
|
|
254
|
+
|
|
255
|
+
// Execute handler with timeout
|
|
256
|
+
const timeoutPromise = new Promise((_, reject) =>
|
|
257
|
+
setTimeout(
|
|
258
|
+
() => reject(new Error(`Job timeout after ${this.jobTimeout}ms`)),
|
|
259
|
+
this.jobTimeout,
|
|
260
|
+
),
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
const handlerPromise = handler(job);
|
|
264
|
+
|
|
265
|
+
await Promise.race([handlerPromise, timeoutPromise]);
|
|
266
|
+
|
|
267
|
+
this._emitEvent("completed", job);
|
|
268
|
+
await this.driver.complete(job.id);
|
|
269
|
+
} catch (error) {
|
|
270
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
271
|
+
const stackTrace = error instanceof Error ? error.stack : undefined;
|
|
272
|
+
|
|
273
|
+
// Determine if we should retry
|
|
274
|
+
if (job.attempts < job.maxRetries) {
|
|
275
|
+
// Exponential backoff: 1s, 2s, 4s, 8s, etc. (capped at 1 hour)
|
|
276
|
+
const delaySeconds = Math.min(Math.pow(2, job.attempts), 3600);
|
|
277
|
+
const delayMs = delaySeconds * 1000;
|
|
278
|
+
|
|
279
|
+
this._emitEvent("retried", job);
|
|
280
|
+
await this.driver.scheduleRetry(job.id, delayMs, errorMsg);
|
|
281
|
+
|
|
282
|
+
console.warn(
|
|
283
|
+
`[JobWorker] Job ${job.id} failed (attempt ${job.attempts}/${job.maxRetries}): ${errorMsg}. Retrying in ${delaySeconds}s`,
|
|
284
|
+
);
|
|
285
|
+
} else {
|
|
286
|
+
this._emitEvent("failed", job);
|
|
287
|
+
await this.driver.fail(job.id, errorMsg, stackTrace);
|
|
288
|
+
|
|
289
|
+
console.error(
|
|
290
|
+
`[JobWorker] Job ${job.id} failed permanently: ${errorMsg}`,
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
} finally {
|
|
294
|
+
this.inFlightJobs.delete(job.id);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Stop the worker gracefully
|
|
300
|
+
* Waits for in-flight jobs to complete before shutdown
|
|
301
|
+
*/
|
|
302
|
+
async stop(): Promise<void> {
|
|
303
|
+
if (!this.isRunning) return;
|
|
304
|
+
|
|
305
|
+
console.log("[JobWorker] Shutting down gracefully...");
|
|
306
|
+
this.isRunning = false;
|
|
307
|
+
|
|
308
|
+
// Wait for in-flight jobs to complete
|
|
309
|
+
const startTime = Date.now();
|
|
310
|
+
while (
|
|
311
|
+
this.inFlightJobs.size > 0 &&
|
|
312
|
+
Date.now() - startTime < this.shutdownTimeout
|
|
313
|
+
) {
|
|
314
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (this.inFlightJobs.size > 0) {
|
|
318
|
+
console.warn(
|
|
319
|
+
`[JobWorker] Force shutdown with ${this.inFlightJobs.size} jobs still in flight`,
|
|
320
|
+
);
|
|
321
|
+
} else {
|
|
322
|
+
console.log("[JobWorker] All jobs completed, shutting down");
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
await this.driver.disconnect();
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Get the number of jobs currently being processed
|
|
330
|
+
*/
|
|
331
|
+
getInFlightCount(): number {
|
|
332
|
+
return this.inFlightJobs.size;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Get queue metrics
|
|
337
|
+
*/
|
|
338
|
+
async getMetrics() {
|
|
339
|
+
return this.driver.getMetrics();
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Check if worker is running
|
|
344
|
+
*/
|
|
345
|
+
isActive(): boolean {
|
|
346
|
+
return this.isRunning;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// ============= Worker CLI Entry Point =============
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Create and start a worker from config
|
|
354
|
+
* Useful for CLI commands like: bueno queue:worker
|
|
355
|
+
*/
|
|
356
|
+
export async function startWorker(config?: JobQueueConfig): Promise<void> {
|
|
357
|
+
const worker = new JobWorker(config);
|
|
358
|
+
|
|
359
|
+
// Log metrics periodically
|
|
360
|
+
const metricsInterval = setInterval(async () => {
|
|
361
|
+
if (worker.isActive()) {
|
|
362
|
+
const metrics = await worker.getMetrics();
|
|
363
|
+
console.log("[JobWorker] Metrics:", {
|
|
364
|
+
pending: metrics.pending,
|
|
365
|
+
processing: metrics.processing,
|
|
366
|
+
processed: metrics.processed,
|
|
367
|
+
failed: metrics.failed,
|
|
368
|
+
avgLatency: `${Math.round(metrics.avgLatency)}ms`,
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
}, 30000);
|
|
372
|
+
|
|
373
|
+
await worker.init();
|
|
374
|
+
|
|
375
|
+
process.on("exit", () => {
|
|
376
|
+
clearInterval(metricsInterval);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
await worker.start();
|
|
380
|
+
}
|
package/src/logger/index.ts
CHANGED
|
@@ -12,7 +12,10 @@ import type { LogEntry, LogLevel, LoggerConfig } from "../index";
|
|
|
12
12
|
/**
|
|
13
13
|
* Error callback for transport errors
|
|
14
14
|
*/
|
|
15
|
-
export type TransportErrorCallback = (
|
|
15
|
+
export type TransportErrorCallback = (
|
|
16
|
+
error: Error,
|
|
17
|
+
transport: LogTransport,
|
|
18
|
+
) => void;
|
|
16
19
|
|
|
17
20
|
/**
|
|
18
21
|
* Base interface for log transports
|
|
@@ -192,7 +195,7 @@ export class HTTPWebhookTransport implements LogTransport {
|
|
|
192
195
|
await this.sleep(delay);
|
|
193
196
|
delay = Math.min(
|
|
194
197
|
delay * this.retryOptions.backoffMultiplier,
|
|
195
|
-
this.retryOptions.maxDelay
|
|
198
|
+
this.retryOptions.maxDelay,
|
|
196
199
|
);
|
|
197
200
|
}
|
|
198
201
|
}
|
|
@@ -259,7 +262,9 @@ export class HTTPWebhookTransport implements LogTransport {
|
|
|
259
262
|
try {
|
|
260
263
|
await this.flush();
|
|
261
264
|
} catch (error) {
|
|
262
|
-
this.handleError(
|
|
265
|
+
this.handleError(
|
|
266
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
267
|
+
);
|
|
263
268
|
}
|
|
264
269
|
}
|
|
265
270
|
}
|
|
@@ -337,7 +342,8 @@ export class DatadogTransport implements LogTransport {
|
|
|
337
342
|
this.env = options.env ?? process.env.NODE_ENV ?? "development";
|
|
338
343
|
this.hostname = options.hostname ?? this.getDefaultHostname();
|
|
339
344
|
this.tags = options.tags ?? [];
|
|
340
|
-
this.endpoint =
|
|
345
|
+
this.endpoint =
|
|
346
|
+
options.endpoint ?? "https://http-intake.logs.datadoghq.com/v1/input";
|
|
341
347
|
this.batchSize = options.batchSize ?? 100;
|
|
342
348
|
this.flushInterval = options.flushInterval ?? 5000;
|
|
343
349
|
this.timeout = options.timeout ?? 30000;
|
|
@@ -395,7 +401,7 @@ export class DatadogTransport implements LogTransport {
|
|
|
395
401
|
*/
|
|
396
402
|
private toDatadogFormat(entry: LogEntry): DatadogLogEntry {
|
|
397
403
|
const allTags = [...this.tags];
|
|
398
|
-
|
|
404
|
+
|
|
399
405
|
// Add environment tag
|
|
400
406
|
if (this.env) {
|
|
401
407
|
allTags.push(`env:${this.env}`);
|
|
@@ -404,7 +410,11 @@ export class DatadogTransport implements LogTransport {
|
|
|
404
410
|
// Add context as tags
|
|
405
411
|
if (entry.context) {
|
|
406
412
|
for (const [key, value] of Object.entries(entry.context)) {
|
|
407
|
-
if (
|
|
413
|
+
if (
|
|
414
|
+
typeof value === "string" ||
|
|
415
|
+
typeof value === "number" ||
|
|
416
|
+
typeof value === "boolean"
|
|
417
|
+
) {
|
|
408
418
|
allTags.push(`${key}:${value}`);
|
|
409
419
|
}
|
|
410
420
|
}
|
|
@@ -437,7 +447,16 @@ export class DatadogTransport implements LogTransport {
|
|
|
437
447
|
|
|
438
448
|
// Add any additional fields from the entry
|
|
439
449
|
for (const [key, value] of Object.entries(entry)) {
|
|
440
|
-
if (
|
|
450
|
+
if (
|
|
451
|
+
![
|
|
452
|
+
"level",
|
|
453
|
+
"message",
|
|
454
|
+
"timestamp",
|
|
455
|
+
"context",
|
|
456
|
+
"error",
|
|
457
|
+
"duration",
|
|
458
|
+
].includes(key)
|
|
459
|
+
) {
|
|
441
460
|
datadogEntry[key] = value;
|
|
442
461
|
}
|
|
443
462
|
}
|
|
@@ -510,7 +529,7 @@ export class DatadogTransport implements LogTransport {
|
|
|
510
529
|
await this.sleep(delay);
|
|
511
530
|
delay = Math.min(
|
|
512
531
|
delay * this.retryOptions.backoffMultiplier,
|
|
513
|
-
this.retryOptions.maxDelay
|
|
532
|
+
this.retryOptions.maxDelay,
|
|
514
533
|
);
|
|
515
534
|
}
|
|
516
535
|
}
|
|
@@ -540,7 +559,9 @@ export class DatadogTransport implements LogTransport {
|
|
|
540
559
|
});
|
|
541
560
|
|
|
542
561
|
if (!response.ok) {
|
|
543
|
-
throw new Error(
|
|
562
|
+
throw new Error(
|
|
563
|
+
`Datadog API error: HTTP ${response.status}: ${response.statusText}`,
|
|
564
|
+
);
|
|
544
565
|
}
|
|
545
566
|
} finally {
|
|
546
567
|
clearTimeout(timeoutId);
|
|
@@ -579,7 +600,9 @@ export class DatadogTransport implements LogTransport {
|
|
|
579
600
|
try {
|
|
580
601
|
await this.flush();
|
|
581
602
|
} catch (error) {
|
|
582
|
-
this.handleError(
|
|
603
|
+
this.handleError(
|
|
604
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
605
|
+
);
|
|
583
606
|
}
|
|
584
607
|
}
|
|
585
608
|
}
|
|
@@ -680,7 +703,10 @@ export class ConsoleTransport implements LogTransport {
|
|
|
680
703
|
this.output(formatted, entry.level);
|
|
681
704
|
} catch (error) {
|
|
682
705
|
if (this.onError) {
|
|
683
|
-
this.onError(
|
|
706
|
+
this.onError(
|
|
707
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
708
|
+
this,
|
|
709
|
+
);
|
|
684
710
|
}
|
|
685
711
|
}
|
|
686
712
|
}
|
|
@@ -766,7 +792,10 @@ export class TransportManager {
|
|
|
766
792
|
await transport.send(entry);
|
|
767
793
|
} catch (error) {
|
|
768
794
|
if (this.onError) {
|
|
769
|
-
this.onError(
|
|
795
|
+
this.onError(
|
|
796
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
797
|
+
transport,
|
|
798
|
+
);
|
|
770
799
|
}
|
|
771
800
|
}
|
|
772
801
|
});
|
|
@@ -783,7 +812,10 @@ export class TransportManager {
|
|
|
783
812
|
await transport.sendBatch(entries);
|
|
784
813
|
} catch (error) {
|
|
785
814
|
if (this.onError) {
|
|
786
|
-
this.onError(
|
|
815
|
+
this.onError(
|
|
816
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
817
|
+
transport,
|
|
818
|
+
);
|
|
787
819
|
}
|
|
788
820
|
}
|
|
789
821
|
});
|
|
@@ -801,7 +833,10 @@ export class TransportManager {
|
|
|
801
833
|
await transport.flush();
|
|
802
834
|
} catch (error) {
|
|
803
835
|
if (this.onError) {
|
|
804
|
-
this.onError(
|
|
836
|
+
this.onError(
|
|
837
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
838
|
+
transport,
|
|
839
|
+
);
|
|
805
840
|
}
|
|
806
841
|
}
|
|
807
842
|
}
|
|
@@ -820,7 +855,10 @@ export class TransportManager {
|
|
|
820
855
|
await transport.close();
|
|
821
856
|
} catch (error) {
|
|
822
857
|
if (this.onError) {
|
|
823
|
-
this.onError(
|
|
858
|
+
this.onError(
|
|
859
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
860
|
+
transport,
|
|
861
|
+
);
|
|
824
862
|
}
|
|
825
863
|
}
|
|
826
864
|
}
|
|
@@ -853,10 +891,12 @@ export class LoggerWithTransports extends Logger {
|
|
|
853
891
|
|
|
854
892
|
constructor(config: LoggerWithTransportsConfig = {}) {
|
|
855
893
|
const { transports, onTransportError, ...loggerConfig } = config;
|
|
856
|
-
|
|
894
|
+
|
|
857
895
|
// Create transport manager
|
|
858
|
-
const transportManager = new TransportManager({
|
|
859
|
-
|
|
896
|
+
const transportManager = new TransportManager({
|
|
897
|
+
onError: onTransportError,
|
|
898
|
+
});
|
|
899
|
+
|
|
860
900
|
// Add transports
|
|
861
901
|
if (transports) {
|
|
862
902
|
for (const transport of transports) {
|
|
@@ -931,7 +971,7 @@ export class LoggerWithTransports extends Logger {
|
|
|
931
971
|
* Create a logger with transports
|
|
932
972
|
*/
|
|
933
973
|
export function createLoggerWithTransports(
|
|
934
|
-
config: LoggerWithTransportsConfig = {}
|
|
974
|
+
config: LoggerWithTransportsConfig = {},
|
|
935
975
|
): LoggerWithTransports {
|
|
936
976
|
return new LoggerWithTransports(config);
|
|
937
977
|
}
|
|
@@ -941,10 +981,10 @@ export function createLoggerWithTransports(
|
|
|
941
981
|
*/
|
|
942
982
|
export function createTransportOutput(
|
|
943
983
|
transports: LogTransport[],
|
|
944
|
-
options?: { onError?: TransportErrorCallback }
|
|
984
|
+
options?: { onError?: TransportErrorCallback },
|
|
945
985
|
): (entry: LogEntry) => void {
|
|
946
986
|
const manager = new TransportManager({ onError: options?.onError });
|
|
947
|
-
|
|
987
|
+
|
|
948
988
|
for (const transport of transports) {
|
|
949
989
|
manager.addTransport(transport);
|
|
950
990
|
}
|
|
@@ -966,4 +1006,4 @@ export default {
|
|
|
966
1006
|
LoggerWithTransports,
|
|
967
1007
|
createLoggerWithTransports,
|
|
968
1008
|
createTransportOutput,
|
|
969
|
-
};
|
|
1009
|
+
};
|
package/src/metrics/index.ts
CHANGED
|
@@ -167,9 +167,7 @@ export async function measureEventLoopLag(): Promise<number> {
|
|
|
167
167
|
/**
|
|
168
168
|
* Measure event loop lag multiple times and return average
|
|
169
169
|
*/
|
|
170
|
-
export async function measureEventLoopLagAverage(
|
|
171
|
-
samples: number = 5,
|
|
172
|
-
): Promise<number> {
|
|
170
|
+
export async function measureEventLoopLagAverage(samples = 5): Promise<number> {
|
|
173
171
|
const measurements: number[] = [];
|
|
174
172
|
|
|
175
173
|
for (let i = 0; i < samples; i++) {
|
|
@@ -243,7 +241,7 @@ export class MetricsCollector {
|
|
|
243
241
|
* Start collecting metrics at regular intervals
|
|
244
242
|
* @param intervalMs Interval in milliseconds (default: 5000)
|
|
245
243
|
*/
|
|
246
|
-
startPeriodicCollection(intervalMs
|
|
244
|
+
startPeriodicCollection(intervalMs = 5000): void {
|
|
247
245
|
if (this.periodicTimer !== null) {
|
|
248
246
|
throw new Error("Periodic collection is already running");
|
|
249
247
|
}
|
|
@@ -310,7 +308,7 @@ export class MetricsCollector {
|
|
|
310
308
|
let sumCpuUser = 0;
|
|
311
309
|
let sumCpuSystem = 0;
|
|
312
310
|
let sumEventLoopLag = 0;
|
|
313
|
-
let minHeapUsed =
|
|
311
|
+
let minHeapUsed = Number.POSITIVE_INFINITY;
|
|
314
312
|
let maxHeapUsed = 0;
|
|
315
313
|
|
|
316
314
|
for (const m of this.history) {
|
|
@@ -333,7 +331,8 @@ export class MetricsCollector {
|
|
|
333
331
|
avgCpuUser: Math.round(sumCpuUser / count),
|
|
334
332
|
avgCpuSystem: Math.round(sumCpuSystem / count),
|
|
335
333
|
avgEventLoopLag: Math.round((sumEventLoopLag / count) * 100) / 100,
|
|
336
|
-
minMemoryHeapUsed:
|
|
334
|
+
minMemoryHeapUsed:
|
|
335
|
+
minHeapUsed === Number.POSITIVE_INFINITY ? 0 : minHeapUsed,
|
|
337
336
|
maxMemoryHeapUsed: maxHeapUsed,
|
|
338
337
|
sampleCount: count,
|
|
339
338
|
timeRange: {
|
|
@@ -377,7 +376,9 @@ export function toPrometheusFormat(metrics: RuntimeMetrics): string {
|
|
|
377
376
|
`process_memory_heap_used_bytes ${metrics.memoryHeapUsed} ${timestamp}`,
|
|
378
377
|
);
|
|
379
378
|
|
|
380
|
-
lines.push(
|
|
379
|
+
lines.push(
|
|
380
|
+
"# HELP process_memory_heap_total_bytes Total heap memory in bytes",
|
|
381
|
+
);
|
|
381
382
|
lines.push("# TYPE process_memory_heap_total_bytes gauge");
|
|
382
383
|
lines.push(
|
|
383
384
|
`process_memory_heap_total_bytes ${metrics.memoryHeapTotal} ${timestamp}`,
|
|
@@ -409,7 +410,9 @@ export function toPrometheusFormat(metrics: RuntimeMetrics): string {
|
|
|
409
410
|
// Runtime metrics
|
|
410
411
|
lines.push("# HELP process_uptime_seconds Process uptime in seconds");
|
|
411
412
|
lines.push("# TYPE process_uptime_seconds gauge");
|
|
412
|
-
lines.push(
|
|
413
|
+
lines.push(
|
|
414
|
+
`process_uptime_seconds ${metrics.uptime.toFixed(2)} ${timestamp}`,
|
|
415
|
+
);
|
|
413
416
|
|
|
414
417
|
lines.push("# HELP nodejs_eventloop_lag_ms Event loop lag in milliseconds");
|
|
415
418
|
lines.push("# TYPE nodejs_eventloop_lag_ms gauge");
|
|
@@ -423,25 +426,29 @@ export function toPrometheusFormat(metrics: RuntimeMetrics): string {
|
|
|
423
426
|
/**
|
|
424
427
|
* Export averaged metrics in Prometheus format
|
|
425
428
|
*/
|
|
426
|
-
export function averagedMetricsToPrometheus(
|
|
427
|
-
averaged: AveragedMetrics,
|
|
428
|
-
): string {
|
|
429
|
+
export function averagedMetricsToPrometheus(averaged: AveragedMetrics): string {
|
|
429
430
|
const timestamp = Date.now();
|
|
430
431
|
const lines: string[] = [];
|
|
431
432
|
|
|
432
|
-
lines.push(
|
|
433
|
+
lines.push(
|
|
434
|
+
"# HELP process_memory_heap_used_avg_bytes Average heap memory used",
|
|
435
|
+
);
|
|
433
436
|
lines.push("# TYPE process_memory_heap_used_avg_bytes gauge");
|
|
434
437
|
lines.push(
|
|
435
438
|
`process_memory_heap_used_avg_bytes ${averaged.avgMemoryHeapUsed} ${timestamp}`,
|
|
436
439
|
);
|
|
437
440
|
|
|
438
|
-
lines.push(
|
|
441
|
+
lines.push(
|
|
442
|
+
"# HELP process_memory_heap_used_min_bytes Minimum heap memory used",
|
|
443
|
+
);
|
|
439
444
|
lines.push("# TYPE process_memory_heap_used_min_bytes gauge");
|
|
440
445
|
lines.push(
|
|
441
446
|
`process_memory_heap_used_min_bytes ${averaged.minMemoryHeapUsed} ${timestamp}`,
|
|
442
447
|
);
|
|
443
448
|
|
|
444
|
-
lines.push(
|
|
449
|
+
lines.push(
|
|
450
|
+
"# HELP process_memory_heap_used_max_bytes Maximum heap memory used",
|
|
451
|
+
);
|
|
445
452
|
lines.push("# TYPE process_memory_heap_used_max_bytes gauge");
|
|
446
453
|
lines.push(
|
|
447
454
|
`process_memory_heap_used_max_bytes ${averaged.maxMemoryHeapUsed} ${timestamp}`,
|
|
@@ -455,7 +462,9 @@ export function averagedMetricsToPrometheus(
|
|
|
455
462
|
|
|
456
463
|
lines.push("# HELP process_metrics_sample_count Number of samples collected");
|
|
457
464
|
lines.push("# TYPE process_metrics_sample_count gauge");
|
|
458
|
-
lines.push(
|
|
465
|
+
lines.push(
|
|
466
|
+
`process_metrics_sample_count ${averaged.sampleCount} ${timestamp}`,
|
|
467
|
+
);
|
|
459
468
|
|
|
460
469
|
return lines.join("\n");
|
|
461
470
|
}
|
|
@@ -491,4 +500,4 @@ export async function collectMetrics(): Promise<RuntimeMetrics> {
|
|
|
491
500
|
eventLoopLag,
|
|
492
501
|
timestamp: new Date().toISOString(),
|
|
493
502
|
};
|
|
494
|
-
}
|
|
503
|
+
}
|