nestworker 2.0.4 → 2.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.md +333 -134
- package/dist/core/worker.interfaces.d.ts +89 -10
- package/dist/core/worker.module.d.ts +21 -11
- package/dist/core/worker.module.js +38 -10
- package/dist/core/worker.module.js.map +1 -1
- package/dist/core/worker.pool.d.ts +26 -6
- package/dist/core/worker.pool.js +246 -101
- package/dist/core/worker.pool.js.map +1 -1
- package/dist/core/worker.service.d.ts +29 -76
- package/dist/core/worker.service.js +108 -80
- package/dist/core/worker.service.js.map +1 -1
- package/dist/decorators/worker-task.decorator.d.ts +7 -25
- package/dist/decorators/worker-task.decorator.js +5 -26
- package/dist/decorators/worker-task.decorator.js.map +1 -1
- package/dist/di/di-serializer.d.ts +9 -8
- package/dist/di/di-serializer.js +69 -46
- package/dist/di/di-serializer.js.map +1 -1
- package/dist/di/worker-container.d.ts +32 -30
- package/dist/di/worker-container.js +130 -70
- package/dist/di/worker-container.js.map +1 -1
- package/dist/discovery/discovery.service.d.ts +2 -9
- package/dist/discovery/discovery.service.js +86 -21
- package/dist/discovery/discovery.service.js.map +1 -1
- package/dist/example/image.service.d.ts +4 -0
- package/dist/example/image.service.js +16 -1
- package/dist/example/image.service.js.map +1 -1
- package/dist/example/main.js +15 -2
- package/dist/example/main.js.map +1 -1
- package/dist/health/worker.health.d.ts +46 -0
- package/dist/health/worker.health.js +77 -0
- package/dist/health/worker.health.js.map +1 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.js +10 -1
- package/dist/index.js.map +1 -1
- package/dist/metrics/worker.metrics.d.ts +65 -0
- package/dist/metrics/worker.metrics.js +122 -0
- package/dist/metrics/worker.metrics.js.map +1 -0
- package/dist/worker/worker-runtime.js +124 -27
- package/dist/worker/worker-runtime.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,19 +15,26 @@
|
|
|
15
15
|
|
|
16
16
|
# nestworker
|
|
17
17
|
|
|
18
|
-
Enterprise-grade worker thread module for NestJS. Offload CPU-bound work to a managed pool of Node.js worker threads without blocking the event loop — with decorator-driven auto-discovery, priority queuing, and transparent NestJS dependency injection inside workers.
|
|
18
|
+
Enterprise-grade worker thread module for NestJS. Offload CPU-bound work to a managed pool of Node.js worker threads without blocking the event loop — with decorator-driven auto-discovery, priority queuing, retry, graceful shutdown, health checks, metrics, and transparent NestJS dependency injection inside workers.
|
|
19
19
|
|
|
20
20
|
---
|
|
21
21
|
|
|
22
22
|
## Features
|
|
23
23
|
|
|
24
|
-
- **Worker pool** — pre-spawned threads
|
|
25
|
-
- **
|
|
24
|
+
- **Worker pool** — pre-spawned threads, warmed up before the first request
|
|
25
|
+
- **Zero cold start** — pool initialises on `onModuleInit`, not on the first call
|
|
26
|
+
- **Priority queue** — `HIGH / NORMAL / LOW`, binary-search sorted; no jobs are ever dropped
|
|
26
27
|
- **Decorator discovery** — `@WorkerClass` + `@WorkerTask` replace all manual registration
|
|
27
|
-
- **
|
|
28
|
-
- **
|
|
29
|
-
- **
|
|
30
|
-
- **
|
|
28
|
+
- **deps** — services serialised into the worker via `vm.runInContext()` + snapshot; use for plain config/data helpers
|
|
29
|
+
- **proxy** — services that stay on the main thread; the worker calls them transparently via IPC round-trip; use for DB, HTTP, queues
|
|
30
|
+
- **Retry + dead letter** — automatic retry with configurable delay; exhausted jobs emit a `dead` event
|
|
31
|
+
- **AbortController** — cancel queued or running tasks via `AbortSignal`
|
|
32
|
+
- **Graceful shutdown** — drains in-flight jobs before terminating workers, with a configurable deadline
|
|
33
|
+
- **Structured error forwarding** — errors preserve `name`, `message`, `stack`, `code`, and custom fields across the thread boundary
|
|
34
|
+
- **AsyncLocalStorage propagation** — ALS context (request ID, tenant, user) is snapshotted and restored inside workers
|
|
35
|
+
- **OpenTelemetry trace propagation** — active span context is injected into each job; no hard dependency
|
|
36
|
+
- **Health indicator** — plugs into `@nestjs/terminus`
|
|
37
|
+
- **Metrics** — counters, per-task duration percentiles (p50/p95/p99); push to any provider
|
|
31
38
|
|
|
32
39
|
---
|
|
33
40
|
|
|
@@ -35,12 +42,14 @@ Enterprise-grade worker thread module for NestJS. Offload CPU-bound work to a ma
|
|
|
35
42
|
|
|
36
43
|
| Package | Version |
|
|
37
44
|
|---|---|
|
|
38
|
-
| Node.js | ≥ 16
|
|
45
|
+
| Node.js | ≥ 16 |
|
|
39
46
|
| `@nestjs/common` | `^10` or `^11` |
|
|
40
47
|
| `@nestjs/core` | `^10` or `^11` |
|
|
41
48
|
| `reflect-metadata` | `^0.1` or `^0.2` |
|
|
42
49
|
| TypeScript `target` | `ES2022` or higher |
|
|
43
50
|
|
|
51
|
+
> **Important:** the project must be compiled to JS before running. nestworker locates compiled files via `require.cache`, which is only populated after compilation. Running via `ts-node` directly is not supported.
|
|
52
|
+
|
|
44
53
|
`tsconfig.json` must have:
|
|
45
54
|
|
|
46
55
|
```json
|
|
@@ -60,26 +69,38 @@ Enterprise-grade worker thread module for NestJS. Offload CPU-bound work to a ma
|
|
|
60
69
|
```bash
|
|
61
70
|
npm install nestworker
|
|
62
71
|
```
|
|
72
|
+
|
|
63
73
|
---
|
|
64
74
|
|
|
65
75
|
## Quick Start
|
|
66
76
|
|
|
67
|
-
### 1.
|
|
77
|
+
### 1. Register `WorkerModule`
|
|
68
78
|
|
|
69
79
|
```ts
|
|
70
80
|
// app.module.ts
|
|
71
81
|
import { Module } from '@nestjs/common';
|
|
72
82
|
import { WorkerModule } from 'nestworker';
|
|
73
|
-
import { ConfigService } from './config.service';
|
|
74
|
-
import { ImageService } from './image.service';
|
|
75
83
|
|
|
76
84
|
@Module({
|
|
77
|
-
imports: [
|
|
78
|
-
|
|
85
|
+
imports: [
|
|
86
|
+
WorkerModule.forRoot({ poolSize: 4 }),
|
|
87
|
+
],
|
|
79
88
|
})
|
|
80
89
|
export class AppModule {}
|
|
81
90
|
```
|
|
82
91
|
|
|
92
|
+
Or async, when options come from `ConfigService`:
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
WorkerModule.forRootAsync({
|
|
96
|
+
inject: [ConfigService],
|
|
97
|
+
useFactory: (cfg: ConfigService) => ({
|
|
98
|
+
poolSize: cfg.get<number>('WORKER_POOL_SIZE'),
|
|
99
|
+
shutdownTimeout: 30_000,
|
|
100
|
+
}),
|
|
101
|
+
})
|
|
102
|
+
```
|
|
103
|
+
|
|
83
104
|
### 2. Decorate your service
|
|
84
105
|
|
|
85
106
|
```ts
|
|
@@ -89,29 +110,21 @@ import { WorkerClass, WorkerTask } from 'nestworker';
|
|
|
89
110
|
import { ConfigService } from './config.service';
|
|
90
111
|
|
|
91
112
|
@Injectable()
|
|
92
|
-
@WorkerClass({ deps: [ConfigService] })
|
|
113
|
+
@WorkerClass({ deps: [ConfigService] })
|
|
93
114
|
export class ImageService {
|
|
94
115
|
constructor(private readonly configService: ConfigService) {}
|
|
95
116
|
|
|
96
|
-
@WorkerTask({ priority: 'HIGH' })
|
|
117
|
+
@WorkerTask({ priority: 'HIGH', timeout: 10_000, retry: 2, retryDelay: 500 })
|
|
97
118
|
resizeImage(value: number): number {
|
|
98
|
-
// runs in a worker thread — configService works normally here
|
|
99
119
|
const multiplier = this.configService.getNumber('MULTIPLIER');
|
|
100
120
|
let total = 0;
|
|
101
121
|
for (let i = 0; i < 10_000_000; i++) total += i * value * multiplier;
|
|
102
122
|
return total;
|
|
103
123
|
}
|
|
104
|
-
|
|
105
|
-
@WorkerTask({ priority: 'NORMAL', timeout: 5000 })
|
|
106
|
-
generateThumbnail(width: number, height: number): string {
|
|
107
|
-
let hash = 0;
|
|
108
|
-
for (let i = 0; i < 5_000_000; i++) hash ^= (i * width * height) | 0;
|
|
109
|
-
return `thumb_${hash.toString(16)}_${width}x${height}.webp`;
|
|
110
|
-
}
|
|
111
124
|
}
|
|
112
125
|
```
|
|
113
126
|
|
|
114
|
-
### 3.
|
|
127
|
+
### 3. Call `run()`
|
|
115
128
|
|
|
116
129
|
```ts
|
|
117
130
|
// image.controller.ts
|
|
@@ -123,201 +136,387 @@ export class ImageController {
|
|
|
123
136
|
constructor(private readonly workerService: WorkerService) {}
|
|
124
137
|
|
|
125
138
|
@Get('resize')
|
|
126
|
-
|
|
139
|
+
resize() {
|
|
127
140
|
return this.workerService.run<number>('ImageService', 'resizeImage', [5]);
|
|
128
141
|
}
|
|
129
|
-
|
|
130
|
-
@Get('thumbnail')
|
|
131
|
-
async thumbnail() {
|
|
132
|
-
return this.workerService.run<string>(
|
|
133
|
-
'ImageService', 'generateThumbnail', [1920, 1080]
|
|
134
|
-
);
|
|
135
|
-
}
|
|
136
142
|
}
|
|
137
143
|
```
|
|
144
|
+
|
|
138
145
|
---
|
|
139
146
|
|
|
140
|
-
##
|
|
147
|
+
## API
|
|
148
|
+
|
|
149
|
+
### `WorkerModule.forRoot(options?)`
|
|
150
|
+
|
|
151
|
+
| Option | Type | Default | Description |
|
|
152
|
+
|---|---|---|---|
|
|
153
|
+
| `poolSize` | `number` | `os.cpus().length` | Worker thread count |
|
|
154
|
+
| `shutdownTimeout` | `number` | `30_000` | Ms to wait for in-flight jobs on shutdown |
|
|
155
|
+
| `asyncLocalStorages` | `AsyncLocalStorage[]` | `[]` | ALS instances to propagate into workers |
|
|
156
|
+
|
|
157
|
+
### `WorkerModule.forRootAsync(options)`
|
|
158
|
+
|
|
159
|
+
| Field | Type | Description |
|
|
160
|
+
|---|---|---|
|
|
161
|
+
| `inject` | `any[]` | Tokens to inject into `useFactory` |
|
|
162
|
+
| `useFactory` | `(...args) => WorkerModuleOptions` | Factory — may be async |
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
### `@WorkerClass(options?)`
|
|
167
|
+
|
|
168
|
+
Marks a NestJS provider as a container of worker tasks.
|
|
169
|
+
|
|
170
|
+
| Option | Type | Description |
|
|
171
|
+
|---|---|---|
|
|
172
|
+
| `deps` | `Type[]` | Services to **serialise** into the worker. Must hold plain cloneable data — no DB connections, sockets, or streams. |
|
|
173
|
+
| `proxy` | `Type[]` | Services that **stay on the main thread**. The worker calls them via IPC. Use for anything with I/O. |
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
### `@WorkerTask(options?)`
|
|
178
|
+
|
|
179
|
+
Marks a method to be offloaded to a worker thread.
|
|
141
180
|
|
|
142
|
-
|
|
181
|
+
| Option | Type | Default | Description |
|
|
182
|
+
|---|---|---|---|
|
|
183
|
+
| `priority` | `'HIGH' \| 'NORMAL' \| 'LOW'` | `'NORMAL'` | Queue priority |
|
|
184
|
+
| `timeout` | `number` | — | Reject after this many ms |
|
|
185
|
+
| `retry` | `number` | `0` | Extra attempts after first failure |
|
|
186
|
+
| `retryDelay` | `number` | `0` | Ms between retry attempts |
|
|
143
187
|
|
|
144
|
-
|
|
188
|
+
---
|
|
145
189
|
|
|
146
|
-
`
|
|
190
|
+
### `WorkerService.run<T>(serviceName, methodName, args?, options?)`
|
|
191
|
+
|
|
192
|
+
| Parameter | Type | Description |
|
|
193
|
+
|---|---|---|
|
|
194
|
+
| `serviceName` | `string` | Class name of the `@WorkerClass` provider |
|
|
195
|
+
| `methodName` | `string` | Method decorated with `@WorkerTask` |
|
|
196
|
+
| `args` | `unknown[]` | structuredClone-compatible arguments |
|
|
197
|
+
| `options` | `RunOptions` | Per-call overrides (see below) |
|
|
147
198
|
|
|
148
199
|
```ts
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
200
|
+
interface RunOptions {
|
|
201
|
+
priority?: TaskPriority;
|
|
202
|
+
timeout?: number;
|
|
203
|
+
retry?: number;
|
|
204
|
+
retryDelay?: number;
|
|
205
|
+
signal?: AbortSignal; // cancel the task
|
|
153
206
|
}
|
|
154
207
|
```
|
|
155
208
|
|
|
156
|
-
|
|
209
|
+
---
|
|
157
210
|
|
|
158
|
-
`
|
|
211
|
+
### `WorkerService` events
|
|
159
212
|
|
|
160
213
|
```ts
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
}
|
|
214
|
+
workerService.onTaskStart((job) => { ... });
|
|
215
|
+
workerService.onTaskEnd((job, durationMs) => { ... });
|
|
216
|
+
workerService.onTaskError((job, error) => { ... });
|
|
217
|
+
workerService.onDead((event) => { ... }); // job exhausted all retries
|
|
166
218
|
```
|
|
167
219
|
|
|
168
|
-
|
|
220
|
+
---
|
|
169
221
|
|
|
170
|
-
|
|
171
|
-
|---|---|
|
|
172
|
-
| Node built-ins: `os`, `path`, `crypto`, `zlib`, `fs` | HTTP clients (`axios`, `fetch`) |
|
|
173
|
-
| Pure computation libraries | Database drivers |
|
|
174
|
-
| `Buffer`, `Math`, `Date` | `Socket`, `Stream` |
|
|
222
|
+
## `deps` vs `proxy`
|
|
175
223
|
|
|
176
|
-
|
|
224
|
+
This is the most important decision when declaring a `@WorkerClass`.
|
|
177
225
|
|
|
178
|
-
|
|
226
|
+
### `deps` — serialise into the worker
|
|
179
227
|
|
|
180
|
-
|
|
228
|
+
The service's compiled `.js` file is executed inside the worker via `vm.runInContext()`. Its instance properties are snapshotted via `structuredClone` and restored. The worker gets a fully independent copy — method calls are local, zero IPC overhead.
|
|
181
229
|
|
|
182
|
-
|
|
230
|
+
**Use when:** the service holds plain data (config values, lookup tables, constants) and its methods are pure computation over that data.
|
|
183
231
|
|
|
184
232
|
```ts
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
233
|
+
// ConfigService holds { multiplier: 3, iterations: 1_000_000 }
|
|
234
|
+
// — plain object, fully cloneable → safe to use as dep
|
|
235
|
+
|
|
236
|
+
@WorkerClass({ deps: [ConfigService] })
|
|
237
|
+
export class ImageService {
|
|
238
|
+
constructor(private readonly configService: ConfigService) {}
|
|
239
|
+
|
|
240
|
+
@WorkerTask()
|
|
241
|
+
resize(value: number): number {
|
|
242
|
+
// configService is a local copy inside the worker — no IPC
|
|
243
|
+
return this.configService.getNumber('MULTIPLIER') * value;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
188
246
|
```
|
|
189
247
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
| `poolSize` | `number` | `os.cpus().length` | Number of worker threads to spawn |
|
|
248
|
+
✅ Plain objects, arrays, primitives, `Map`, `Set`
|
|
249
|
+
❌ DB connections, HTTP clients, sockets, streams, open file handles
|
|
193
250
|
|
|
194
|
-
|
|
251
|
+
### `proxy` — stay on the main thread, call via IPC
|
|
195
252
|
|
|
196
|
-
|
|
253
|
+
The service is **not** sent to the worker. Instead, a lightweight stub is injected whose methods send an `ipc:invoke` message to the main thread and return a `Promise` that resolves when the main thread replies. The real NestJS service executes on the main thread with full access to DB, HTTP, and everything else.
|
|
197
254
|
|
|
198
|
-
|
|
255
|
+
**Use when:** the service does I/O — database queries, HTTP calls, cache reads, queue operations.
|
|
199
256
|
|
|
200
257
|
```ts
|
|
201
|
-
|
|
202
|
-
|
|
258
|
+
// UserService queries a database — cannot be cloned → use proxy
|
|
259
|
+
|
|
260
|
+
@WorkerClass({ proxy: [UserService] })
|
|
261
|
+
export class ReportService {
|
|
262
|
+
constructor(private readonly userService: UserService) {}
|
|
263
|
+
|
|
264
|
+
@WorkerTask()
|
|
265
|
+
async generateReport(userId: string): Promise<string> {
|
|
266
|
+
// this call transparently round-trips to the main thread
|
|
267
|
+
const user = await this.userService.findById(userId);
|
|
268
|
+
|
|
269
|
+
// heavy CPU work runs in the worker
|
|
270
|
+
return crunchNumbers(user);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
203
273
|
```
|
|
204
274
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
275
|
+
The IPC round-trip looks like this:
|
|
276
|
+
|
|
277
|
+
```
|
|
278
|
+
WORKER MAIN THREAD
|
|
279
|
+
────────────────────────────────────── ───────────────────────────────
|
|
280
|
+
this.userService.findById(userId)
|
|
281
|
+
│
|
|
282
|
+
├─ postMessage({ type: 'ipc:invoke', → onMessage handler
|
|
283
|
+
│ method: 'findById', args: [...] }) │
|
|
284
|
+
│ ├─ userService.findById(userId)
|
|
285
|
+
│ │ (real DB query, main thread)
|
|
286
|
+
│ │
|
|
287
|
+
◀── postMessage({ type: 'ipc:result', ─── └─ reply with result
|
|
288
|
+
data: { id, name, ... } })
|
|
289
|
+
│
|
|
290
|
+
└─ Promise resolves with user ✓
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
> **Constraint:** proxy method arguments and return values must be `structuredClone`-compatible — they cross the thread boundary via `postMessage`. Plain objects, arrays, and primitives work. Class instances, functions, and sockets do not.
|
|
294
|
+
|
|
295
|
+
### Using both together
|
|
296
|
+
|
|
297
|
+
`deps` and `proxy` can be combined in the same `@WorkerClass`:
|
|
298
|
+
|
|
299
|
+
```ts
|
|
300
|
+
@WorkerClass({
|
|
301
|
+
deps: [ConfigService], // cloned into worker — fast local access
|
|
302
|
+
proxy: [UserService], // stays on main thread — IPC on each call
|
|
303
|
+
})
|
|
304
|
+
export class ReportService {
|
|
305
|
+
constructor(
|
|
306
|
+
private readonly configService: ConfigService,
|
|
307
|
+
private readonly userService: UserService,
|
|
308
|
+
) {}
|
|
309
|
+
|
|
310
|
+
@WorkerTask({ priority: 'LOW' })
|
|
311
|
+
async buildReport(userId: string): Promise<Buffer> {
|
|
312
|
+
const limit = this.configService.getNumber('REPORT_LIMIT'); // local, zero IPC
|
|
313
|
+
const user = await this.userService.findById(userId); // IPC round-trip
|
|
314
|
+
return heavyPdfGeneration(user, limit);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
```
|
|
208
318
|
|
|
209
319
|
---
|
|
210
320
|
|
|
211
|
-
|
|
321
|
+
## AbortController
|
|
212
322
|
|
|
213
|
-
|
|
323
|
+
Cancel a queued or running task by passing an `AbortSignal`:
|
|
214
324
|
|
|
215
325
|
```ts
|
|
216
|
-
|
|
217
|
-
|
|
326
|
+
const controller = new AbortController();
|
|
327
|
+
|
|
328
|
+
// Cancel after 3 seconds if not done
|
|
329
|
+
setTimeout(() => controller.abort(), 3000);
|
|
330
|
+
|
|
331
|
+
try {
|
|
332
|
+
const result = await workerService.run(
|
|
333
|
+
'ImageService', 'resizeImage', [5],
|
|
334
|
+
{ signal: controller.signal },
|
|
335
|
+
);
|
|
336
|
+
} catch (err) {
|
|
337
|
+
if (err.name === 'AbortError') {
|
|
338
|
+
console.log('Task was cancelled');
|
|
339
|
+
}
|
|
340
|
+
}
|
|
218
341
|
```
|
|
219
342
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
343
|
+
The `AbortSignal` is also injected as the last argument of the task method, so you can respond to cancellation inside the worker:
|
|
344
|
+
|
|
345
|
+
```ts
|
|
346
|
+
@WorkerTask()
|
|
347
|
+
processChunks(data: number[], signal: AbortSignal): number {
|
|
348
|
+
let total = 0;
|
|
349
|
+
for (const chunk of data) {
|
|
350
|
+
if (signal.aborted) break; // stop early on cancel
|
|
351
|
+
total += heavyCompute(chunk);
|
|
352
|
+
}
|
|
353
|
+
return total;
|
|
354
|
+
}
|
|
355
|
+
```
|
|
224
356
|
|
|
225
357
|
---
|
|
226
358
|
|
|
227
|
-
|
|
359
|
+
## Retry and Dead Letter
|
|
228
360
|
|
|
229
|
-
|
|
361
|
+
```ts
|
|
362
|
+
@WorkerTask({ retry: 3, retryDelay: 1000 })
|
|
363
|
+
async fetchAndProcess(id: string): Promise<string> { ... }
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
After all attempts fail, a `dead` event fires:
|
|
230
367
|
|
|
231
368
|
```ts
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
//
|
|
236
|
-
|
|
237
|
-
'ImageService', 'resizeImage', [5],
|
|
238
|
-
{ priority: 'LOW', timeout: 10_000 }
|
|
239
|
-
);
|
|
369
|
+
workerService.onDead((event) => {
|
|
370
|
+
console.error(`Job ${event.jobId} failed after ${event.attempts} attempts`);
|
|
371
|
+
console.error(event.error.message);
|
|
372
|
+
// push to external DLQ, alert, etc.
|
|
373
|
+
});
|
|
240
374
|
```
|
|
241
375
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
| `methodName` | `string` | Method name decorated with `@WorkerTask` |
|
|
246
|
-
| `args` | `unknown[]` | Arguments to pass — must be structuredClone-compatible |
|
|
247
|
-
| `overrides` | `object` | Optional `priority` / `timeout` override for this call |
|
|
376
|
+
---
|
|
377
|
+
|
|
378
|
+
## Graceful Shutdown
|
|
248
379
|
|
|
249
|
-
|
|
380
|
+
On application shutdown, nestworker waits up to `shutdownTimeout` ms for in-flight jobs to complete before force-terminating workers. Queued jobs that haven't started are rejected immediately.
|
|
381
|
+
|
|
382
|
+
```ts
|
|
383
|
+
WorkerModule.forRoot({ shutdownTimeout: 30_000 })
|
|
384
|
+
```
|
|
250
385
|
|
|
251
386
|
---
|
|
252
387
|
|
|
253
|
-
##
|
|
388
|
+
## AsyncLocalStorage Propagation
|
|
254
389
|
|
|
255
|
-
|
|
390
|
+
Pass your ALS instances to `forRoot` — their current store is snapshotted at dispatch time and restored inside the worker before the task runs:
|
|
256
391
|
|
|
257
|
-
|
|
392
|
+
```ts
|
|
393
|
+
export const requestAls = new AsyncLocalStorage<{ requestId: string }>();
|
|
258
394
|
|
|
259
|
-
|
|
395
|
+
WorkerModule.forRoot({
|
|
396
|
+
asyncLocalStorages: [requestAls],
|
|
397
|
+
})
|
|
260
398
|
|
|
261
|
-
|
|
399
|
+
// Inside a worker task:
|
|
400
|
+
@WorkerTask()
|
|
401
|
+
process(): void {
|
|
402
|
+
const store = requestAls.getStore(); // { requestId: '...' } ✓
|
|
403
|
+
}
|
|
404
|
+
```
|
|
262
405
|
|
|
263
|
-
|
|
406
|
+
---
|
|
264
407
|
|
|
265
|
-
|
|
408
|
+
## Health Indicator
|
|
266
409
|
|
|
267
|
-
|
|
410
|
+
```ts
|
|
411
|
+
// health.module.ts
|
|
412
|
+
import { WorkerHealthIndicator } from 'nestworker';
|
|
268
413
|
|
|
269
|
-
|
|
414
|
+
@Module({ providers: [WorkerHealthIndicator] })
|
|
415
|
+
export class HealthModule {}
|
|
416
|
+
```
|
|
270
417
|
|
|
418
|
+
```ts
|
|
419
|
+
// health.controller.ts
|
|
420
|
+
import { Controller, Get } from '@nestjs/common';
|
|
421
|
+
import { HealthCheck, HealthCheckService } from '@nestjs/terminus';
|
|
422
|
+
import { WorkerHealthIndicator } from 'nestworker';
|
|
423
|
+
|
|
424
|
+
@Controller('health')
|
|
425
|
+
export class HealthController {
|
|
426
|
+
constructor(
|
|
427
|
+
private readonly health: HealthCheckService,
|
|
428
|
+
private readonly workerHealth: WorkerHealthIndicator,
|
|
429
|
+
) {}
|
|
430
|
+
|
|
431
|
+
@Get()
|
|
432
|
+
@HealthCheck()
|
|
433
|
+
check() {
|
|
434
|
+
return this.health.check([
|
|
435
|
+
() => this.workerHealth.check('workers'),
|
|
436
|
+
]);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
271
439
|
```
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
440
|
+
|
|
441
|
+
Reports `down` when workers are still warming up or queue depth exceeds pool size.
|
|
442
|
+
|
|
443
|
+
---
|
|
444
|
+
|
|
445
|
+
## Metrics
|
|
446
|
+
|
|
447
|
+
```ts
|
|
448
|
+
// app.module.ts
|
|
449
|
+
import { WorkerMetricsService } from 'nestworker';
|
|
450
|
+
|
|
451
|
+
@Module({ providers: [WorkerMetricsService] })
|
|
452
|
+
export class AppModule {}
|
|
283
453
|
```
|
|
284
454
|
|
|
285
|
-
|
|
455
|
+
```ts
|
|
456
|
+
// metrics.controller.ts
|
|
457
|
+
import { WorkerMetricsService } from 'nestworker';
|
|
286
458
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
459
|
+
@Controller('metrics')
|
|
460
|
+
export class MetricsController {
|
|
461
|
+
constructor(private readonly workerMetrics: WorkerMetricsService) {}
|
|
462
|
+
|
|
463
|
+
@Get()
|
|
464
|
+
snapshot() {
|
|
465
|
+
return this.workerMetrics.snapshot();
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
```json
|
|
471
|
+
{
|
|
472
|
+
"jobsTotal": 1500,
|
|
473
|
+
"jobsSuccess": 1480,
|
|
474
|
+
"jobsFailed": 15,
|
|
475
|
+
"jobsTimeout": 3,
|
|
476
|
+
"jobsDead": 2,
|
|
477
|
+
"queueDepth": 4,
|
|
478
|
+
"idleWorkers": 2,
|
|
479
|
+
"busyWorkers": 6,
|
|
480
|
+
"durations": {
|
|
481
|
+
"ImageService.resizeImage": { "p50": 42, "p95": 310, "p99": 890, "count": 1200 },
|
|
482
|
+
"ReportService.buildReport": { "p50": 180, "p95": 950, "p99": 2100, "count": 300 }
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
```
|
|
291
486
|
|
|
292
487
|
---
|
|
293
488
|
|
|
294
489
|
## Priority Queue
|
|
295
490
|
|
|
296
|
-
Jobs queue when all threads are busy
|
|
491
|
+
Jobs queue when all threads are busy, sorted by priority — `HIGH` always runs before `NORMAL` before `LOW`. Within the same priority, FIFO.
|
|
297
492
|
|
|
298
493
|
```ts
|
|
299
|
-
// These four tasks are dispatched to the pool concurrently.
|
|
300
|
-
// HIGH tasks run first regardless of arrival order.
|
|
301
494
|
await Promise.all([
|
|
302
|
-
workerService.run('Svc', '
|
|
303
|
-
workerService.run('Svc', '
|
|
304
|
-
workerService.run('Svc', '
|
|
305
|
-
workerService.run('Svc', '
|
|
495
|
+
workerService.run('Svc', 'task', [], { priority: 'LOW' }),
|
|
496
|
+
workerService.run('Svc', 'task', [], { priority: 'HIGH' }),
|
|
497
|
+
workerService.run('Svc', 'task', [], { priority: 'NORMAL' }),
|
|
498
|
+
workerService.run('Svc', 'task', [], { priority: 'HIGH' }),
|
|
306
499
|
]);
|
|
500
|
+
// Execution order: HIGH → HIGH → NORMAL → LOW
|
|
307
501
|
```
|
|
502
|
+
|
|
308
503
|
---
|
|
309
504
|
|
|
310
505
|
## Constraints
|
|
311
506
|
|
|
312
|
-
### Arguments and
|
|
507
|
+
### Arguments and return values
|
|
313
508
|
|
|
314
|
-
|
|
509
|
+
Must be [structuredClone](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm) compatible.
|
|
315
510
|
|
|
316
511
|
| ✅ Supported | ❌ Not supported |
|
|
317
512
|
|---|---|
|
|
318
|
-
| Primitives, plain objects, arrays | Class instances |
|
|
319
|
-
| `Map`, `Set`, `ArrayBuffer` | Functions |
|
|
320
|
-
| `TypedArray`, `DataView` | `Promise`, `WeakMap` |
|
|
513
|
+
| Primitives, plain objects, arrays | Class instances with methods |
|
|
514
|
+
| `Map`, `Set`, `ArrayBuffer`, `Buffer` | Functions, closures |
|
|
515
|
+
| `TypedArray`, `DataView` | `Promise`, `WeakMap`, `Socket` |
|
|
516
|
+
|
|
517
|
+
### Compiled output required
|
|
518
|
+
|
|
519
|
+
nestworker locates class files via `require.cache`. The project must be compiled to `.js` before running — `ts-node` is not supported.
|
|
321
520
|
|
|
322
521
|
### Circular deps
|
|
323
522
|
|
|
@@ -327,7 +526,7 @@ Circular dependencies between `@WorkerClass({ deps })` entries are not supported
|
|
|
327
526
|
|
|
328
527
|
## Contributing
|
|
329
528
|
|
|
330
|
-
See the [contributing guide](https://github.com/VaheHak/nestworker/blob/master/CONTRIBUTING.md)
|
|
529
|
+
See the [contributing guide](https://github.com/VaheHak/nestworker/blob/master/CONTRIBUTING.md).
|
|
331
530
|
|
|
332
531
|
## License
|
|
333
532
|
|