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