@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,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Job Queue Factory and API
|
|
3
|
+
*
|
|
4
|
+
* Main public interface for enqueueing and managing background jobs.
|
|
5
|
+
* Supports both Redis and in-memory drivers.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { MemoryJobQueueDriver } from "./drivers/memory";
|
|
9
|
+
import { RedisJobQueueDriver } from "./drivers/redis";
|
|
10
|
+
import type {
|
|
11
|
+
HandlerRegistryEntry,
|
|
12
|
+
Job,
|
|
13
|
+
JobEvent,
|
|
14
|
+
JobEventType,
|
|
15
|
+
JobHandler,
|
|
16
|
+
JobQueueConfig,
|
|
17
|
+
JobQueueDriver,
|
|
18
|
+
QueueMetrics,
|
|
19
|
+
QueueOptions,
|
|
20
|
+
} from "./types";
|
|
21
|
+
|
|
22
|
+
// ============= Job Queue Class =============
|
|
23
|
+
|
|
24
|
+
export class JobQueue {
|
|
25
|
+
private driver: JobQueueDriver;
|
|
26
|
+
private handlers: Map<string, JobHandler<unknown>> = new Map();
|
|
27
|
+
private handlerRegistry: HandlerRegistryEntry[] = [];
|
|
28
|
+
private eventListeners: Map<JobEventType, Set<(event: JobEvent) => void>> =
|
|
29
|
+
new Map();
|
|
30
|
+
private isRunning = false;
|
|
31
|
+
|
|
32
|
+
constructor(config: JobQueueConfig = {}) {
|
|
33
|
+
// Instantiate appropriate driver
|
|
34
|
+
const driver = config.driver ?? "memory";
|
|
35
|
+
|
|
36
|
+
if (driver === "redis") {
|
|
37
|
+
if (!config.url) {
|
|
38
|
+
throw new Error("Redis URL is required for Redis driver");
|
|
39
|
+
}
|
|
40
|
+
this.driver = new RedisJobQueueDriver(config);
|
|
41
|
+
} else {
|
|
42
|
+
this.driver = new MemoryJobQueueDriver(config);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Initialize the queue (connect to backend)
|
|
48
|
+
*/
|
|
49
|
+
async init(): Promise<void> {
|
|
50
|
+
await this.driver.connect();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Shutdown the queue gracefully
|
|
55
|
+
*/
|
|
56
|
+
async shutdown(): Promise<void> {
|
|
57
|
+
this.isRunning = false;
|
|
58
|
+
await this.driver.disconnect();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Enqueue a job
|
|
63
|
+
* @param name - Job name/type (e.g., "email.welcome")
|
|
64
|
+
* @param data - Job payload
|
|
65
|
+
* @param options - Queue options (delay, priority, timeout)
|
|
66
|
+
* @returns Job ID
|
|
67
|
+
*/
|
|
68
|
+
async enqueue<T = unknown>(
|
|
69
|
+
name: string,
|
|
70
|
+
data: T,
|
|
71
|
+
options?: QueueOptions,
|
|
72
|
+
): Promise<string> {
|
|
73
|
+
const jobId = await this.driver.enqueue(name, data, options);
|
|
74
|
+
this._emitEvent("enqueued", { id: jobId, name } as unknown as Job);
|
|
75
|
+
return jobId;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Register a handler for a job type
|
|
80
|
+
* Supports wildcards: "email.*" matches "email.welcome", "email.reset", etc.
|
|
81
|
+
*/
|
|
82
|
+
on(pattern: string, handler: JobHandler<unknown>): void {
|
|
83
|
+
this.handlers.set(pattern, handler);
|
|
84
|
+
|
|
85
|
+
// Calculate specificity for wildcard matching (longer = more specific)
|
|
86
|
+
const specificity = pattern.split(".").length;
|
|
87
|
+
const entry: HandlerRegistryEntry = { pattern, handler, specificity };
|
|
88
|
+
|
|
89
|
+
// Insert in order of specificity (highest first)
|
|
90
|
+
const index = this.handlerRegistry.findIndex(
|
|
91
|
+
(e) => e.specificity < specificity,
|
|
92
|
+
);
|
|
93
|
+
if (index >= 0) {
|
|
94
|
+
this.handlerRegistry.splice(index, 0, entry);
|
|
95
|
+
} else {
|
|
96
|
+
this.handlerRegistry.push(entry);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Remove a handler
|
|
102
|
+
*/
|
|
103
|
+
off(pattern: string): void {
|
|
104
|
+
this.handlers.delete(pattern);
|
|
105
|
+
const index = this.handlerRegistry.findIndex((e) => e.pattern === pattern);
|
|
106
|
+
if (index >= 0) {
|
|
107
|
+
this.handlerRegistry.splice(index, 1);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Find the best matching handler for a job
|
|
113
|
+
*/
|
|
114
|
+
private findHandler(jobName: string): JobHandler<unknown> | null {
|
|
115
|
+
for (const entry of this.handlerRegistry) {
|
|
116
|
+
if (this._patternMatches(entry.pattern, jobName)) {
|
|
117
|
+
return entry.handler;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Check if a pattern matches a job name (supports wildcards)
|
|
125
|
+
*/
|
|
126
|
+
private _patternMatches(pattern: string, jobName: string): boolean {
|
|
127
|
+
if (pattern === jobName) return true;
|
|
128
|
+
|
|
129
|
+
// Handle wildcards: "email.*" matches "email.welcome"
|
|
130
|
+
if (pattern.endsWith(".*")) {
|
|
131
|
+
const prefix = pattern.slice(0, -2); // Remove ".*"
|
|
132
|
+
return jobName.startsWith(prefix + ".");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Listen for queue events
|
|
140
|
+
*/
|
|
141
|
+
onEvent(event: JobEventType, listener: (event: JobEvent) => void): void {
|
|
142
|
+
if (!this.eventListeners.has(event)) {
|
|
143
|
+
this.eventListeners.set(event, new Set());
|
|
144
|
+
}
|
|
145
|
+
this.eventListeners.get(event)!.add(listener);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Stop listening for queue events
|
|
150
|
+
*/
|
|
151
|
+
offEvent(event: JobEventType, listener: (event: JobEvent) => void): void {
|
|
152
|
+
this.eventListeners.get(event)?.delete(listener);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Emit a queue event
|
|
157
|
+
*/
|
|
158
|
+
private _emitEvent(eventType: JobEventType, job: Job): void {
|
|
159
|
+
const listeners = this.eventListeners.get(eventType);
|
|
160
|
+
if (!listeners) return;
|
|
161
|
+
|
|
162
|
+
const event: JobEvent = {
|
|
163
|
+
type: eventType,
|
|
164
|
+
job,
|
|
165
|
+
timestamp: new Date(),
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
for (const listener of listeners) {
|
|
169
|
+
try {
|
|
170
|
+
listener(event);
|
|
171
|
+
} catch (error) {
|
|
172
|
+
console.error(`Error in ${eventType} listener:`, error);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Start the polling loop (for tests/manual control)
|
|
179
|
+
* Note: In production, use a separate worker process
|
|
180
|
+
*/
|
|
181
|
+
async start(
|
|
182
|
+
options: { pollInterval?: number; concurrency?: number } = {},
|
|
183
|
+
): Promise<void> {
|
|
184
|
+
this.isRunning = true;
|
|
185
|
+
const pollInterval = options.pollInterval ?? 1000;
|
|
186
|
+
const concurrency = options.concurrency ?? 10;
|
|
187
|
+
|
|
188
|
+
// This is a basic polling loop for simple use cases
|
|
189
|
+
// For production, use a dedicated worker process with JobWorker class
|
|
190
|
+
while (this.isRunning) {
|
|
191
|
+
try {
|
|
192
|
+
const jobs = await this.driver.claim(concurrency, 30000);
|
|
193
|
+
|
|
194
|
+
for (const job of jobs) {
|
|
195
|
+
if (!this.isRunning) break;
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
const handler = this.findHandler(job.name);
|
|
199
|
+
if (!handler) {
|
|
200
|
+
console.warn(`No handler found for job type: ${job.name}`);
|
|
201
|
+
await this.driver.complete(job.id);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
this._emitEvent("started", job);
|
|
206
|
+
|
|
207
|
+
await handler(job);
|
|
208
|
+
|
|
209
|
+
this._emitEvent("completed", job);
|
|
210
|
+
await this.driver.complete(job.id);
|
|
211
|
+
} catch (error) {
|
|
212
|
+
const errorMsg =
|
|
213
|
+
error instanceof Error ? error.message : String(error);
|
|
214
|
+
const stackTrace = error instanceof Error ? error.stack : undefined;
|
|
215
|
+
|
|
216
|
+
if (job.attempts < job.maxRetries) {
|
|
217
|
+
const backoffMs = Math.pow(2, job.attempts) * 1000; // Exponential backoff
|
|
218
|
+
this._emitEvent("retried", job);
|
|
219
|
+
await this.driver.scheduleRetry(job.id, backoffMs, errorMsg);
|
|
220
|
+
} else {
|
|
221
|
+
this._emitEvent("failed", job);
|
|
222
|
+
await this.driver.fail(job.id, errorMsg, stackTrace);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Sleep before next poll
|
|
228
|
+
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
|
229
|
+
} catch (error) {
|
|
230
|
+
console.error("Error in job queue polling loop:", error);
|
|
231
|
+
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Stop the polling loop
|
|
238
|
+
*/
|
|
239
|
+
async stop(): Promise<void> {
|
|
240
|
+
this.isRunning = false;
|
|
241
|
+
// Wait a bit for current jobs to finish
|
|
242
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Get a job by ID
|
|
247
|
+
*/
|
|
248
|
+
async getJob(jobId: string): Promise<Job | null> {
|
|
249
|
+
return this.driver.getJob(jobId);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Get queue metrics
|
|
254
|
+
*/
|
|
255
|
+
async getMetrics(): Promise<QueueMetrics> {
|
|
256
|
+
return this.driver.getMetrics();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Clear all jobs (use with caution!)
|
|
261
|
+
*/
|
|
262
|
+
async clear(): Promise<void> {
|
|
263
|
+
return this.driver.clear();
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Check if queue is connected
|
|
268
|
+
*/
|
|
269
|
+
async isConnected(): Promise<boolean> {
|
|
270
|
+
return this.driver.isConnected();
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ============= Factory Function =============
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Create a new job queue instance
|
|
278
|
+
*/
|
|
279
|
+
export function createJobQueue(config?: JobQueueConfig): JobQueue {
|
|
280
|
+
return new JobQueue(config);
|
|
281
|
+
}
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Background Jobs / Task Queue
|
|
3
|
+
*
|
|
4
|
+
* Type definitions for the job queue system.
|
|
5
|
+
* Supports Redis (production) and in-memory (development) drivers.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ============= Job Status =============
|
|
9
|
+
|
|
10
|
+
export type JobStatus =
|
|
11
|
+
| "pending"
|
|
12
|
+
| "processing"
|
|
13
|
+
| "completed"
|
|
14
|
+
| "failed"
|
|
15
|
+
| "delayed";
|
|
16
|
+
|
|
17
|
+
// ============= Job Data Structure =============
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Represents a background job
|
|
21
|
+
*/
|
|
22
|
+
export interface Job<T = unknown> {
|
|
23
|
+
/** Unique job identifier */
|
|
24
|
+
id: string;
|
|
25
|
+
|
|
26
|
+
/** Job name/type (e.g., "email.welcome", "image.resize") */
|
|
27
|
+
name: string;
|
|
28
|
+
|
|
29
|
+
/** Job payload data */
|
|
30
|
+
data: T;
|
|
31
|
+
|
|
32
|
+
/** Current status of the job */
|
|
33
|
+
status: JobStatus;
|
|
34
|
+
|
|
35
|
+
/** Number of times this job has been attempted */
|
|
36
|
+
attempts: number;
|
|
37
|
+
|
|
38
|
+
/** Maximum number of retry attempts */
|
|
39
|
+
maxRetries: number;
|
|
40
|
+
|
|
41
|
+
/** Error message if job failed */
|
|
42
|
+
error?: string;
|
|
43
|
+
|
|
44
|
+
/** Stack trace if job failed */
|
|
45
|
+
stackTrace?: string;
|
|
46
|
+
|
|
47
|
+
/** When the job was created (ISO 8601) */
|
|
48
|
+
createdAt: string;
|
|
49
|
+
|
|
50
|
+
/** When the job was last updated (ISO 8601) */
|
|
51
|
+
updatedAt: string;
|
|
52
|
+
|
|
53
|
+
/** When the job should start (ISO 8601, for delayed jobs) */
|
|
54
|
+
scheduledFor?: string;
|
|
55
|
+
|
|
56
|
+
/** When job processing started (ISO 8601) */
|
|
57
|
+
startedAt?: string;
|
|
58
|
+
|
|
59
|
+
/** When job processing completed (ISO 8601) */
|
|
60
|
+
completedAt?: string;
|
|
61
|
+
|
|
62
|
+
/** How long the job took to complete in milliseconds */
|
|
63
|
+
duration?: number;
|
|
64
|
+
|
|
65
|
+
/** Job priority (higher = process sooner) */
|
|
66
|
+
priority?: number;
|
|
67
|
+
|
|
68
|
+
/** Custom metadata attached to the job */
|
|
69
|
+
metadata?: Record<string, unknown>;
|
|
70
|
+
|
|
71
|
+
/** Worker that claimed this job */
|
|
72
|
+
workerId?: string;
|
|
73
|
+
|
|
74
|
+
/** When the job claim expires (for dead letter handling) */
|
|
75
|
+
claimExpiresAt?: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ============= Queue Configuration =============
|
|
79
|
+
|
|
80
|
+
export interface JobQueueConfig {
|
|
81
|
+
/** Driver type: 'redis' for production, 'memory' for development */
|
|
82
|
+
driver?: "redis" | "memory";
|
|
83
|
+
|
|
84
|
+
/** Redis connection URL (required if driver is 'redis') */
|
|
85
|
+
url?: string;
|
|
86
|
+
|
|
87
|
+
/** Key prefix for all jobs (default: 'jobs:') */
|
|
88
|
+
keyPrefix?: string;
|
|
89
|
+
|
|
90
|
+
/** Maximum number of jobs to process concurrently (default: 10) */
|
|
91
|
+
concurrency?: number;
|
|
92
|
+
|
|
93
|
+
/** Maximum number of retry attempts (default: 3) */
|
|
94
|
+
maxRetries?: number;
|
|
95
|
+
|
|
96
|
+
/** Base delay in seconds for exponential backoff (default: 1) */
|
|
97
|
+
retryDelay?: number;
|
|
98
|
+
|
|
99
|
+
/** Number of jobs to claim in a single poll (default: 10) */
|
|
100
|
+
batchSize?: number;
|
|
101
|
+
|
|
102
|
+
/** Polling interval in milliseconds (default: 1000) */
|
|
103
|
+
pollInterval?: number;
|
|
104
|
+
|
|
105
|
+
/** Job timeout in milliseconds (default: 300000 / 5 minutes) */
|
|
106
|
+
jobTimeout?: number;
|
|
107
|
+
|
|
108
|
+
/** Enable metrics collection (default: true) */
|
|
109
|
+
enableMetrics?: boolean;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ============= Enqueue Options =============
|
|
113
|
+
|
|
114
|
+
export interface QueueOptions {
|
|
115
|
+
/** Schedule job for later execution (ISO 8601 or Date) */
|
|
116
|
+
delay?: number | Date;
|
|
117
|
+
|
|
118
|
+
/** Job priority (default: 0) */
|
|
119
|
+
priority?: number;
|
|
120
|
+
|
|
121
|
+
/** Custom metadata */
|
|
122
|
+
metadata?: Record<string, unknown>;
|
|
123
|
+
|
|
124
|
+
/** Override default job timeout in milliseconds */
|
|
125
|
+
timeout?: number;
|
|
126
|
+
|
|
127
|
+
/** Maximum retries for this specific job */
|
|
128
|
+
maxRetries?: number;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ============= Handler Type =============
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Async handler function for processing jobs
|
|
135
|
+
*/
|
|
136
|
+
export type JobHandler<T = unknown> = (job: Job<T>) => Promise<void>;
|
|
137
|
+
|
|
138
|
+
// ============= Queue Metrics =============
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Metrics for job queue observability
|
|
142
|
+
*/
|
|
143
|
+
export interface QueueMetrics {
|
|
144
|
+
/** Total jobs enqueued */
|
|
145
|
+
enqueued: number;
|
|
146
|
+
|
|
147
|
+
/** Jobs successfully completed */
|
|
148
|
+
processed: number;
|
|
149
|
+
|
|
150
|
+
/** Jobs that failed (exceeded max retries) */
|
|
151
|
+
failed: number;
|
|
152
|
+
|
|
153
|
+
/** Jobs currently in pending state */
|
|
154
|
+
pending: number;
|
|
155
|
+
|
|
156
|
+
/** Jobs currently being processed */
|
|
157
|
+
processing: number;
|
|
158
|
+
|
|
159
|
+
/** Average job processing time in milliseconds */
|
|
160
|
+
avgLatency: number;
|
|
161
|
+
|
|
162
|
+
/** Total processing time in milliseconds */
|
|
163
|
+
totalLatency: number;
|
|
164
|
+
|
|
165
|
+
/** Number of job retries that occurred */
|
|
166
|
+
retried: number;
|
|
167
|
+
|
|
168
|
+
/** Success rate (0.0 to 1.0) */
|
|
169
|
+
successRate: number;
|
|
170
|
+
|
|
171
|
+
/** Average number of attempts per job */
|
|
172
|
+
avgAttempts: number;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ============= Events =============
|
|
176
|
+
|
|
177
|
+
export type JobEventType =
|
|
178
|
+
| "enqueued"
|
|
179
|
+
| "started"
|
|
180
|
+
| "completed"
|
|
181
|
+
| "failed"
|
|
182
|
+
| "retried";
|
|
183
|
+
|
|
184
|
+
export interface JobEvent<T = unknown> {
|
|
185
|
+
type: JobEventType;
|
|
186
|
+
job: Job<T>;
|
|
187
|
+
timestamp: Date;
|
|
188
|
+
metadata?: Record<string, unknown>;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ============= Lock Handle for Job Claims =============
|
|
192
|
+
|
|
193
|
+
export interface JobClaimHandle {
|
|
194
|
+
/** Whether the claim was successfully acquired */
|
|
195
|
+
acquired: boolean;
|
|
196
|
+
|
|
197
|
+
/** Release the claim (job goes back to pending) */
|
|
198
|
+
release: () => Promise<boolean>;
|
|
199
|
+
|
|
200
|
+
/** Extend the claim TTL for long-running jobs */
|
|
201
|
+
extend: (ttl?: number) => Promise<boolean>;
|
|
202
|
+
|
|
203
|
+
/** Check if claim is still valid */
|
|
204
|
+
isValid: () => Promise<boolean>;
|
|
205
|
+
|
|
206
|
+
/** Get remaining TTL in milliseconds */
|
|
207
|
+
getRemainingTTL: () => Promise<number>;
|
|
208
|
+
|
|
209
|
+
/** The claim key */
|
|
210
|
+
key: string;
|
|
211
|
+
|
|
212
|
+
/** The claim value (unique identifier) */
|
|
213
|
+
value: string;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ============= Driver Interface =============
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Interface that both Redis and Memory drivers must implement
|
|
220
|
+
*/
|
|
221
|
+
export interface JobQueueDriver {
|
|
222
|
+
/**
|
|
223
|
+
* Initialize the driver (e.g., connect to Redis)
|
|
224
|
+
*/
|
|
225
|
+
connect(): Promise<void>;
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Disconnect from the driver
|
|
229
|
+
*/
|
|
230
|
+
disconnect(): Promise<void>;
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Check if connected
|
|
234
|
+
*/
|
|
235
|
+
isConnected(): Promise<boolean>;
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Enqueue a new job
|
|
239
|
+
*/
|
|
240
|
+
enqueue<T = unknown>(
|
|
241
|
+
name: string,
|
|
242
|
+
data: T,
|
|
243
|
+
options?: QueueOptions,
|
|
244
|
+
): Promise<string>;
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Claim a batch of pending jobs for processing
|
|
248
|
+
*/
|
|
249
|
+
claim(count: number, timeout: number): Promise<Job[]>;
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Mark a job as completed
|
|
253
|
+
*/
|
|
254
|
+
complete(jobId: string): Promise<void>;
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Mark a job as failed
|
|
258
|
+
*/
|
|
259
|
+
fail(jobId: string, error: string, stackTrace?: string): Promise<void>;
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Schedule a job for retry
|
|
263
|
+
*/
|
|
264
|
+
scheduleRetry(jobId: string, delayMs: number, error: string): Promise<void>;
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Get a job by ID
|
|
268
|
+
*/
|
|
269
|
+
getJob(jobId: string): Promise<Job | null>;
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Get queue metrics
|
|
273
|
+
*/
|
|
274
|
+
getMetrics(): Promise<QueueMetrics>;
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Clear all jobs from the queue (use with caution!)
|
|
278
|
+
*/
|
|
279
|
+
clear(): Promise<void>;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ============= Handler Registry Entry =============
|
|
283
|
+
|
|
284
|
+
export interface HandlerRegistryEntry {
|
|
285
|
+
pattern: string;
|
|
286
|
+
handler: JobHandler<unknown>;
|
|
287
|
+
specificity: number; // For wildcard matching priority
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ============= Configuration Validation =============
|
|
291
|
+
|
|
292
|
+
export interface JobConfigValidationResult {
|
|
293
|
+
valid: boolean;
|
|
294
|
+
errors: string[];
|
|
295
|
+
}
|