nestworker 2.0.8 → 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 +343 -117
- 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 +26 -74
- package/dist/core/worker.service.js +105 -79
- 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/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/main.js +14 -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,174 +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
|
|
141
148
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
|
145
|
-
|
|
146
|
-
| `
|
|
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 |
|
|
147
163
|
|
|
148
164
|
---
|
|
149
165
|
|
|
150
|
-
|
|
166
|
+
### `@WorkerClass(options?)`
|
|
151
167
|
|
|
152
|
-
|
|
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. |
|
|
153
174
|
|
|
154
|
-
|
|
175
|
+
---
|
|
155
176
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
})
|
|
160
|
-
```
|
|
177
|
+
### `@WorkerTask(options?)`
|
|
178
|
+
|
|
179
|
+
Marks a method to be offloaded to a worker thread.
|
|
161
180
|
|
|
162
181
|
| Option | Type | Default | Description |
|
|
163
182
|
|---|---|---|---|
|
|
164
|
-
| `
|
|
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 |
|
|
165
187
|
|
|
166
188
|
---
|
|
167
189
|
|
|
168
|
-
###
|
|
190
|
+
### `WorkerService.run<T>(serviceName, methodName, args?, options?)`
|
|
169
191
|
|
|
170
|
-
|
|
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) |
|
|
171
198
|
|
|
172
199
|
```ts
|
|
173
|
-
|
|
174
|
-
|
|
200
|
+
interface RunOptions {
|
|
201
|
+
priority?: TaskPriority;
|
|
202
|
+
timeout?: number;
|
|
203
|
+
retry?: number;
|
|
204
|
+
retryDelay?: number;
|
|
205
|
+
signal?: AbortSignal; // cancel the task
|
|
206
|
+
}
|
|
175
207
|
```
|
|
176
208
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
### `WorkerService` events
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
workerService.onTaskStart((job) => { ... });
|
|
215
|
+
workerService.onTaskEnd((job, durationMs) => { ... });
|
|
216
|
+
workerService.onTaskError((job, error) => { ... });
|
|
217
|
+
workerService.onDead((event) => { ... }); // job exhausted all retries
|
|
218
|
+
```
|
|
180
219
|
|
|
181
220
|
---
|
|
182
221
|
|
|
183
|
-
|
|
222
|
+
## `deps` vs `proxy`
|
|
223
|
+
|
|
224
|
+
This is the most important decision when declaring a `@WorkerClass`.
|
|
225
|
+
|
|
226
|
+
### `deps` — serialise into the worker
|
|
227
|
+
|
|
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.
|
|
229
|
+
|
|
230
|
+
**Use when:** the service holds plain data (config values, lookup tables, constants) and its methods are pure computation over that data.
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
// ConfigService holds { multiplier: 3, iterations: 1_000_000 }
|
|
234
|
+
// — plain object, fully cloneable → safe to use as dep
|
|
184
235
|
|
|
185
|
-
|
|
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
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
✅ Plain objects, arrays, primitives, `Map`, `Set`
|
|
249
|
+
❌ DB connections, HTTP clients, sockets, streams, open file handles
|
|
250
|
+
|
|
251
|
+
### `proxy` — stay on the main thread, call via IPC
|
|
252
|
+
|
|
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.
|
|
254
|
+
|
|
255
|
+
**Use when:** the service does I/O — database queries, HTTP calls, cache reads, queue operations.
|
|
186
256
|
|
|
187
257
|
```ts
|
|
188
|
-
|
|
189
|
-
|
|
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
|
+
}
|
|
190
273
|
```
|
|
191
274
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
+
```
|
|
196
318
|
|
|
197
319
|
---
|
|
198
320
|
|
|
199
|
-
|
|
321
|
+
## AbortController
|
|
200
322
|
|
|
201
|
-
|
|
323
|
+
Cancel a queued or running task by passing an `AbortSignal`:
|
|
202
324
|
|
|
203
325
|
```ts
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
+
}
|
|
212
341
|
```
|
|
213
342
|
|
|
214
|
-
|
|
215
|
-
|---|---|---|
|
|
216
|
-
| `serviceName` | `string` | Class name of the `@WorkerClass` provider |
|
|
217
|
-
| `methodName` | `string` | Method name decorated with `@WorkerTask` |
|
|
218
|
-
| `args` | `unknown[]` | Arguments to pass — must be structuredClone-compatible |
|
|
219
|
-
| `overrides` | `object` | Optional `priority` / `timeout` override for this call |
|
|
343
|
+
The `AbortSignal` is also injected as the last argument of the task method, so you can respond to cancellation inside the worker:
|
|
220
344
|
|
|
221
|
-
|
|
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
|
+
```
|
|
222
356
|
|
|
223
357
|
---
|
|
224
358
|
|
|
225
|
-
##
|
|
359
|
+
## Retry and Dead Letter
|
|
360
|
+
|
|
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:
|
|
367
|
+
|
|
368
|
+
```ts
|
|
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
|
+
});
|
|
374
|
+
```
|
|
226
375
|
|
|
227
|
-
|
|
376
|
+
---
|
|
228
377
|
|
|
229
|
-
|
|
378
|
+
## Graceful Shutdown
|
|
230
379
|
|
|
231
|
-
|
|
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.
|
|
232
381
|
|
|
233
|
-
|
|
382
|
+
```ts
|
|
383
|
+
WorkerModule.forRoot({ shutdownTimeout: 30_000 })
|
|
384
|
+
```
|
|
234
385
|
|
|
235
|
-
|
|
386
|
+
---
|
|
236
387
|
|
|
237
|
-
|
|
388
|
+
## AsyncLocalStorage Propagation
|
|
238
389
|
|
|
239
|
-
|
|
390
|
+
Pass your ALS instances to `forRoot` — their current store is snapshotted at dispatch time and restored inside the worker before the task runs:
|
|
240
391
|
|
|
241
|
-
|
|
392
|
+
```ts
|
|
393
|
+
export const requestAls = new AsyncLocalStorage<{ requestId: string }>();
|
|
242
394
|
|
|
243
|
-
|
|
395
|
+
WorkerModule.forRoot({
|
|
396
|
+
asyncLocalStorages: [requestAls],
|
|
397
|
+
})
|
|
244
398
|
|
|
399
|
+
// Inside a worker task:
|
|
400
|
+
@WorkerTask()
|
|
401
|
+
process(): void {
|
|
402
|
+
const store = requestAls.getStore(); // { requestId: '...' } ✓
|
|
403
|
+
}
|
|
245
404
|
```
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
405
|
+
|
|
406
|
+
---
|
|
407
|
+
|
|
408
|
+
## Health Indicator
|
|
409
|
+
|
|
410
|
+
```ts
|
|
411
|
+
// health.module.ts
|
|
412
|
+
import { WorkerHealthIndicator } from 'nestworker';
|
|
413
|
+
|
|
414
|
+
@Module({ providers: [WorkerHealthIndicator] })
|
|
415
|
+
export class HealthModule {}
|
|
256
416
|
```
|
|
257
417
|
|
|
258
|
-
|
|
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
|
+
}
|
|
439
|
+
```
|
|
259
440
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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 {}
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
```ts
|
|
456
|
+
// metrics.controller.ts
|
|
457
|
+
import { WorkerMetricsService } from 'nestworker';
|
|
458
|
+
|
|
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
|
+
```
|
|
264
486
|
|
|
265
487
|
---
|
|
266
488
|
|
|
267
489
|
## Priority Queue
|
|
268
490
|
|
|
269
|
-
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.
|
|
270
492
|
|
|
271
493
|
```ts
|
|
272
|
-
// These four tasks are dispatched to the pool concurrently.
|
|
273
|
-
// HIGH tasks run first regardless of arrival order.
|
|
274
494
|
await Promise.all([
|
|
275
|
-
workerService.run('Svc', '
|
|
276
|
-
workerService.run('Svc', '
|
|
277
|
-
workerService.run('Svc', '
|
|
278
|
-
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' }),
|
|
279
499
|
]);
|
|
500
|
+
// Execution order: HIGH → HIGH → NORMAL → LOW
|
|
280
501
|
```
|
|
502
|
+
|
|
281
503
|
---
|
|
282
504
|
|
|
283
505
|
## Constraints
|
|
284
506
|
|
|
285
|
-
### Arguments and
|
|
507
|
+
### Arguments and return values
|
|
286
508
|
|
|
287
|
-
|
|
509
|
+
Must be [structuredClone](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm) compatible.
|
|
288
510
|
|
|
289
511
|
| ✅ Supported | ❌ Not supported |
|
|
290
512
|
|---|---|
|
|
291
|
-
| Primitives, plain objects, arrays | Class instances |
|
|
292
|
-
| `Map`, `Set`, `ArrayBuffer` | Functions |
|
|
293
|
-
| `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.
|
|
294
520
|
|
|
295
521
|
### Circular deps
|
|
296
522
|
|
|
@@ -300,7 +526,7 @@ Circular dependencies between `@WorkerClass({ deps })` entries are not supported
|
|
|
300
526
|
|
|
301
527
|
## Contributing
|
|
302
528
|
|
|
303
|
-
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).
|
|
304
530
|
|
|
305
531
|
## License
|
|
306
532
|
|