@m16khb/nestjs-sidequest 0.1.0
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.ko.md +402 -0
- package/README.md +402 -0
- package/dist/index.cjs +1037 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +900 -0
- package/dist/index.d.ts +900 -0
- package/dist/index.js +1014 -0
- package/dist/index.js.map +1 -0
- package/package.json +85 -0
package/README.md
ADDED
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
# @m16khb/nestjs-sidequest
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@m16khb/nestjs-sidequest)
|
|
4
|
+
[](https://www.gnu.org/licenses/lgpl-3.0.html)
|
|
5
|
+
[](https://www.typescriptlang.org/)
|
|
6
|
+
[](https://nestjs.com/)
|
|
7
|
+
|
|
8
|
+
[English](#) | [한국어](https://github.com/m16khb/npm-library/blob/main/packages/nestjs-sidequest/README.ko.md)
|
|
9
|
+
|
|
10
|
+
**NestJS integration for [Sidequest.js](https://sidequestjs.com/)** - Database-native background job processing without Redis.
|
|
11
|
+
|
|
12
|
+
Process background jobs using your existing database (PostgreSQL, MySQL, MongoDB, SQLite) with full NestJS decorator support and optional CLS integration.
|
|
13
|
+
|
|
14
|
+
## Features
|
|
15
|
+
|
|
16
|
+
- **Database-Native Jobs** - Use your existing database instead of Redis
|
|
17
|
+
- **Transaction Consistency** - Atomic job creation within database transactions
|
|
18
|
+
- **Decorator-Based API** - Familiar `@Processor`, `@OnJob`, `@Retry` decorators
|
|
19
|
+
- **Optional CLS Support** - Context propagation with nestjs-cls
|
|
20
|
+
- **Event Handlers** - `@OnJobComplete`, `@OnJobFailed` for job lifecycle events
|
|
21
|
+
- **Multiple Queue Support** - Configure queues with individual concurrency settings
|
|
22
|
+
- **Dashboard** - Built-in UI for monitoring jobs (optional)
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install @m16khb/nestjs-sidequest sidequest
|
|
28
|
+
|
|
29
|
+
# Choose your database backend
|
|
30
|
+
npm install @sidequest/postgres-backend # PostgreSQL
|
|
31
|
+
npm install @sidequest/mysql-backend # MySQL
|
|
32
|
+
npm install @sidequest/sqlite-backend # SQLite
|
|
33
|
+
npm install @sidequest/mongo-backend # MongoDB
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Optional Dependencies
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
# For CLS context propagation
|
|
40
|
+
pnpm add nestjs-cls
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Requirements
|
|
44
|
+
|
|
45
|
+
- Node.js >= 22.6.0
|
|
46
|
+
- NestJS >= 10.0.0
|
|
47
|
+
- TypeScript >= 5.7
|
|
48
|
+
- A supported database backend
|
|
49
|
+
|
|
50
|
+
## Quick Start
|
|
51
|
+
|
|
52
|
+
### 1. Register Module
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
// app.module.ts
|
|
56
|
+
import { Module } from '@nestjs/common';
|
|
57
|
+
import { SidequestModule } from '@m16khb/nestjs-sidequest';
|
|
58
|
+
|
|
59
|
+
@Module({
|
|
60
|
+
imports: [
|
|
61
|
+
SidequestModule.forRoot({
|
|
62
|
+
backend: {
|
|
63
|
+
driver: '@sidequest/postgres-backend',
|
|
64
|
+
config: process.env.DATABASE_URL,
|
|
65
|
+
},
|
|
66
|
+
queues: [
|
|
67
|
+
{ name: 'email', concurrency: 5 },
|
|
68
|
+
{ name: 'reports', concurrency: 2 },
|
|
69
|
+
],
|
|
70
|
+
}),
|
|
71
|
+
],
|
|
72
|
+
})
|
|
73
|
+
export class AppModule {}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### 2. Define a Job Class (Sidequest.js Pattern)
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
// jobs/send-welcome-email.job.ts
|
|
80
|
+
import { Job } from 'sidequest';
|
|
81
|
+
|
|
82
|
+
export class SendWelcomeEmailJob extends Job {
|
|
83
|
+
constructor(
|
|
84
|
+
public readonly to: string,
|
|
85
|
+
public readonly name: string,
|
|
86
|
+
) {
|
|
87
|
+
super();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### 3. Create a Processor
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
// email.processor.ts
|
|
96
|
+
import { Processor, OnJob, Retry, OnJobComplete, OnJobFailed } from '@m16khb/nestjs-sidequest';
|
|
97
|
+
import { SendWelcomeEmailJob } from './jobs/send-welcome-email.job';
|
|
98
|
+
|
|
99
|
+
@Processor('email')
|
|
100
|
+
export class EmailProcessor {
|
|
101
|
+
constructor(private readonly mailer: MailerService) {}
|
|
102
|
+
|
|
103
|
+
@OnJob(SendWelcomeEmailJob)
|
|
104
|
+
@Retry({ maxAttempts: 3, backoff: { type: 'exponential', delay: 1000 } })
|
|
105
|
+
async handleWelcomeEmail(job: SendWelcomeEmailJob) {
|
|
106
|
+
await this.mailer.send({
|
|
107
|
+
to: job.to,
|
|
108
|
+
subject: `Welcome, ${job.name}!`,
|
|
109
|
+
template: 'welcome',
|
|
110
|
+
});
|
|
111
|
+
return { sentAt: new Date() };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
@OnJobComplete(SendWelcomeEmailJob)
|
|
115
|
+
async onComplete(event: JobCompleteEvent) {
|
|
116
|
+
console.log(`Email sent at: ${event.result.sentAt}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
@OnJobFailed(SendWelcomeEmailJob)
|
|
120
|
+
async onFailed(event: JobFailedEvent) {
|
|
121
|
+
console.error(`Email failed: ${event.error.message}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### 4. Inject Queue and Add Jobs
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
// user.service.ts
|
|
130
|
+
import { Injectable } from '@nestjs/common';
|
|
131
|
+
import { InjectQueue, IQueueService } from '@m16khb/nestjs-sidequest';
|
|
132
|
+
import { SendWelcomeEmailJob } from './jobs/send-welcome-email.job';
|
|
133
|
+
|
|
134
|
+
@Injectable()
|
|
135
|
+
export class UserService {
|
|
136
|
+
constructor(
|
|
137
|
+
@InjectQueue('email') private emailQueue: IQueueService,
|
|
138
|
+
) {}
|
|
139
|
+
|
|
140
|
+
async createUser(email: string, name: string) {
|
|
141
|
+
// Create user in database...
|
|
142
|
+
const user = await this.userRepository.save({ email, name });
|
|
143
|
+
|
|
144
|
+
// Queue welcome email (runs in background)
|
|
145
|
+
await this.emailQueue.add(SendWelcomeEmailJob, email, name);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async scheduleWelcomeEmail(email: string, name: string, sendAt: Date) {
|
|
149
|
+
await this.emailQueue.addScheduled(
|
|
150
|
+
SendWelcomeEmailJob,
|
|
151
|
+
sendAt,
|
|
152
|
+
email,
|
|
153
|
+
name
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Module Configuration
|
|
160
|
+
|
|
161
|
+
### forRoot (Synchronous)
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
SidequestModule.forRoot({
|
|
165
|
+
// Module
|
|
166
|
+
isGlobal: true, // Default: true
|
|
167
|
+
|
|
168
|
+
// Database Backend
|
|
169
|
+
backend: {
|
|
170
|
+
driver: '@sidequest/postgres-backend',
|
|
171
|
+
config: process.env.DATABASE_URL,
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
// Queues
|
|
175
|
+
queues: [
|
|
176
|
+
{
|
|
177
|
+
name: 'email',
|
|
178
|
+
concurrency: 5, // Max concurrent jobs
|
|
179
|
+
priority: 50, // Default priority (higher = first)
|
|
180
|
+
state: 'active', // 'active' | 'paused'
|
|
181
|
+
},
|
|
182
|
+
],
|
|
183
|
+
|
|
184
|
+
// Engine Settings
|
|
185
|
+
maxConcurrentJobs: 10, // Global max concurrency
|
|
186
|
+
minThreads: 4, // Min worker threads (default: CPU cores)
|
|
187
|
+
maxThreads: 8, // Max worker threads (default: minThreads * 2)
|
|
188
|
+
jobPollingInterval: 100, // Job polling interval (ms)
|
|
189
|
+
releaseStaleJobsIntervalMin: 60, // Stale job release interval (minutes)
|
|
190
|
+
cleanupFinishedJobsIntervalMin: 60, // Finished job cleanup interval (minutes)
|
|
191
|
+
|
|
192
|
+
// Logger
|
|
193
|
+
logger: {
|
|
194
|
+
level: 'info',
|
|
195
|
+
json: false, // JSON output for production
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
// Dashboard (Optional)
|
|
199
|
+
dashboard: {
|
|
200
|
+
enabled: true,
|
|
201
|
+
port: 8678,
|
|
202
|
+
path: '/',
|
|
203
|
+
auth: {
|
|
204
|
+
user: 'admin',
|
|
205
|
+
password: 'password',
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
|
|
209
|
+
// Graceful Shutdown
|
|
210
|
+
gracefulShutdown: {
|
|
211
|
+
enabled: true,
|
|
212
|
+
timeout: 30000, // 30 seconds
|
|
213
|
+
},
|
|
214
|
+
|
|
215
|
+
// CLS Integration (Optional)
|
|
216
|
+
enableCls: true, // Requires nestjs-cls to be installed
|
|
217
|
+
})
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### forRootAsync (Asynchronous)
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
SidequestModule.forRootAsync({
|
|
224
|
+
imports: [ConfigModule],
|
|
225
|
+
useFactory: (config: ConfigService) => ({
|
|
226
|
+
backend: {
|
|
227
|
+
driver: '@sidequest/postgres-backend',
|
|
228
|
+
config: config.get('DATABASE_URL'),
|
|
229
|
+
},
|
|
230
|
+
queues: [
|
|
231
|
+
{ name: 'email', concurrency: config.get('EMAIL_CONCURRENCY', 5) },
|
|
232
|
+
],
|
|
233
|
+
enableCls: config.get('ENABLE_CLS', false),
|
|
234
|
+
}),
|
|
235
|
+
inject: [ConfigService],
|
|
236
|
+
})
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## Decorators
|
|
240
|
+
|
|
241
|
+
### @Processor(queueName, options?)
|
|
242
|
+
|
|
243
|
+
Marks a class as a job processor for the specified queue.
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
@Processor('email', { concurrency: 10 })
|
|
247
|
+
export class EmailProcessor {}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### @OnJob(JobClass, options?)
|
|
251
|
+
|
|
252
|
+
Marks a method as a handler for the specified job type.
|
|
253
|
+
|
|
254
|
+
```typescript
|
|
255
|
+
@OnJob(SendEmailJob, { timeout: 30000 })
|
|
256
|
+
async handleEmail(job: SendEmailJob) {
|
|
257
|
+
// ...
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### @Retry(options)
|
|
262
|
+
|
|
263
|
+
Configures retry policy for a job handler.
|
|
264
|
+
|
|
265
|
+
```typescript
|
|
266
|
+
@Retry({
|
|
267
|
+
maxAttempts: 3,
|
|
268
|
+
backoff: {
|
|
269
|
+
type: 'exponential', // 'exponential' | 'fixed'
|
|
270
|
+
delay: 1000, // Initial delay in ms
|
|
271
|
+
multiplier: 2, // Exponential multiplier
|
|
272
|
+
},
|
|
273
|
+
retryOn: ['NetworkError', 'TimeoutError'], // Retry only on these errors
|
|
274
|
+
})
|
|
275
|
+
async handleJob(job: AnyJob) {
|
|
276
|
+
// ...
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### @InjectQueue(queueName)
|
|
281
|
+
|
|
282
|
+
Injects a queue service instance.
|
|
283
|
+
|
|
284
|
+
```typescript
|
|
285
|
+
constructor(@InjectQueue('email') private emailQueue: IQueueService) {}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### @OnJobComplete(JobClass?)
|
|
289
|
+
|
|
290
|
+
Handler called when a job completes successfully.
|
|
291
|
+
|
|
292
|
+
```typescript
|
|
293
|
+
@OnJobComplete(SendEmailJob)
|
|
294
|
+
async onComplete(event: JobCompleteEvent) {
|
|
295
|
+
console.log(`Job ${event.jobId} completed:`, event.result);
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### @OnJobFailed(JobClass?)
|
|
300
|
+
|
|
301
|
+
Handler called when a job fails.
|
|
302
|
+
|
|
303
|
+
```typescript
|
|
304
|
+
@OnJobFailed(SendEmailJob)
|
|
305
|
+
async onFailed(event: JobFailedEvent) {
|
|
306
|
+
console.error(`Job ${event.jobId} failed:`, event.error);
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
## Queue Service API
|
|
311
|
+
|
|
312
|
+
```typescript
|
|
313
|
+
interface IQueueService {
|
|
314
|
+
readonly name: string;
|
|
315
|
+
|
|
316
|
+
// Add a single job
|
|
317
|
+
add<T>(JobClass: new (...args: unknown[]) => T, ...args: Parameters<T['constructor']>): Promise<string>;
|
|
318
|
+
|
|
319
|
+
// Add a job with options
|
|
320
|
+
addWithOptions<T>(
|
|
321
|
+
JobClass: new (...args: unknown[]) => T,
|
|
322
|
+
options: JobAddOptions,
|
|
323
|
+
...args: Parameters<T['constructor']>
|
|
324
|
+
): Promise<string>;
|
|
325
|
+
|
|
326
|
+
// Add a scheduled job
|
|
327
|
+
addScheduled<T>(
|
|
328
|
+
JobClass: new (...args: unknown[]) => T,
|
|
329
|
+
scheduledAt: Date,
|
|
330
|
+
...args: Parameters<T['constructor']>
|
|
331
|
+
): Promise<string>;
|
|
332
|
+
|
|
333
|
+
// Add multiple jobs (bulk)
|
|
334
|
+
addBulk<T>(jobs: Array<{
|
|
335
|
+
JobClass: new (...args: unknown[]) => T;
|
|
336
|
+
args: Parameters<T['constructor']>;
|
|
337
|
+
options?: JobAddOptions;
|
|
338
|
+
}>): Promise<string[]>;
|
|
339
|
+
}
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
### JobAddOptions
|
|
343
|
+
|
|
344
|
+
```typescript
|
|
345
|
+
interface JobAddOptions {
|
|
346
|
+
priority?: number; // Higher = processed first (default: 50)
|
|
347
|
+
timeout?: number; // Job timeout in ms
|
|
348
|
+
maxAttempts?: number; // Override retry attempts
|
|
349
|
+
startAfter?: Date; // Delayed start
|
|
350
|
+
}
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
## CLS Integration
|
|
354
|
+
|
|
355
|
+
Enable CLS integration to propagate context (traceId, userId, etc.) across job executions:
|
|
356
|
+
|
|
357
|
+
```typescript
|
|
358
|
+
// app.module.ts
|
|
359
|
+
SidequestModule.forRoot({
|
|
360
|
+
// ...
|
|
361
|
+
enableCls: true, // Requires nestjs-cls
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
// Context is automatically propagated
|
|
365
|
+
@Processor('email')
|
|
366
|
+
export class EmailProcessor {
|
|
367
|
+
constructor(private readonly cls: ClsService) {}
|
|
368
|
+
|
|
369
|
+
@OnJob(SendEmailJob)
|
|
370
|
+
async handleEmail(job: SendEmailJob) {
|
|
371
|
+
const traceId = this.cls.getId();
|
|
372
|
+
const userId = this.cls.get('userId');
|
|
373
|
+
|
|
374
|
+
console.log(`[${traceId}] Processing job for user ${userId}`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
## Why Sidequest.js?
|
|
380
|
+
|
|
381
|
+
| Feature | BullMQ + Redis | Sidequest.js |
|
|
382
|
+
|---------|----------------|--------------|
|
|
383
|
+
| Infrastructure | Additional Redis server | Uses existing database |
|
|
384
|
+
| Transaction Support | Requires compensation transactions | Native DB transaction support |
|
|
385
|
+
| Operational Cost | Extra Redis instance cost | No additional infrastructure |
|
|
386
|
+
| Deployment Simplicity | Manage Redis cluster | Simple database connection |
|
|
387
|
+
|
|
388
|
+
## License
|
|
389
|
+
|
|
390
|
+
**LGPL v3** - This library is licensed under the GNU Lesser General Public License v3.0.
|
|
391
|
+
|
|
392
|
+
This means:
|
|
393
|
+
- You may use this library in proprietary software without opening your source code
|
|
394
|
+
- If you modify this library itself, modifications must be released under LGPL/GPL
|
|
395
|
+
- You must provide license attribution and allow users to replace the library
|
|
396
|
+
- Dynamic linking is recommended for compliance
|
|
397
|
+
|
|
398
|
+
For full license text, see [LICENSE](LICENSE).
|
|
399
|
+
|
|
400
|
+
---
|
|
401
|
+
|
|
402
|
+
This package integrates [Sidequest.js](https://sidequestjs.com/), which is also licensed under LGPL v3.
|