nestworker 2.1.6 → 2.1.7
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 +684 -648
- package/dist/core/worker.interfaces.d.ts +38 -3
- package/dist/core/worker.module.js.map +1 -1
- package/dist/core/worker.pool.d.ts +52 -15
- package/dist/core/worker.pool.js +373 -216
- package/dist/core/worker.pool.js.map +1 -1
- package/dist/core/worker.service.d.ts +37 -3
- package/dist/core/worker.service.js +88 -16
- package/dist/core/worker.service.js.map +1 -1
- package/dist/di/di-serializer.js +24 -15
- package/dist/di/di-serializer.js.map +1 -1
- package/dist/di/worker-container.d.ts +24 -5
- package/dist/di/worker-container.js +70 -30
- package/dist/di/worker-container.js.map +1 -1
- package/dist/discovery/discovery.service.js +6 -15
- package/dist/discovery/discovery.service.js.map +1 -1
- package/dist/example/bench.js +8 -2
- package/dist/example/bench.js.map +1 -1
- package/dist/example/image.service.d.ts +8 -0
- package/dist/example/image.service.js +31 -0
- package/dist/example/image.service.js.map +1 -1
- package/dist/example/main.js +10 -2
- package/dist/example/main.js.map +1 -1
- package/dist/health/worker.health.js +3 -1
- package/dist/health/worker.health.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/metrics/worker.metrics.js +6 -1
- package/dist/metrics/worker.metrics.js.map +1 -1
- package/dist/worker/worker-runtime.js +81 -73
- package/dist/worker/worker-runtime.js.map +1 -1
- package/package.json +85 -82
package/README.md
CHANGED
|
@@ -1,648 +1,684 @@
|
|
|
1
|
-

|
|
2
|
-

|
|
3
|
-

|
|
4
|
-

|
|
5
|
-

|
|
6
|
-

|
|
7
|
-

|
|
8
|
-

|
|
9
|
-

|
|
10
|
-

|
|
11
|
-
|
|
12
|
-
<p align="center">
|
|
13
|
-
<img src="icon.svg" width="120" alt="nestworker" />
|
|
14
|
-
</p>
|
|
15
|
-
|
|
16
|
-
# nestworker
|
|
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, retry, graceful shutdown, health checks, metrics, and transparent NestJS dependency injection inside workers.
|
|
19
|
-
|
|
20
|
-
---
|
|
21
|
-
|
|
22
|
-
## Table of Contents
|
|
23
|
-
|
|
24
|
-
- [Features](#features)
|
|
25
|
-
- [Requirements](#requirements)
|
|
26
|
-
- [Installation](#installation)
|
|
27
|
-
- [Quick Start](#quick-start)
|
|
28
|
-
- [1. Register `WorkerModule`](#1-register-workermodule)
|
|
29
|
-
- [2. Decorate your service](#2-decorate-your-service)
|
|
30
|
-
- [3. Call `run()`](#3-call-run)
|
|
31
|
-
- [API](#api)
|
|
32
|
-
- [`WorkerModule.forRoot(options?)`](#workermoduleforrootoptions)
|
|
33
|
-
- [`WorkerModule.forRootAsync(options)`](#workermoduleforrootasyncoptions)
|
|
34
|
-
- [`@WorkerClass(options?)`](#workerclassoptions)
|
|
35
|
-
- [`@WorkerTask(options?)`](#workertaskoptions)
|
|
36
|
-
- [`WorkerService.run<T>(serviceName, methodName, args?, options?)`](#workerserviceruntservicename-methodname-args-options)
|
|
37
|
-
- [`WorkerService` events](#workerservice-events)
|
|
38
|
-
- [`WorkerService.stats()`](#workerservicestats)
|
|
39
|
-
- [`deps` vs `proxy`](#deps-vs-proxy)
|
|
40
|
-
- [`deps` — serialise into the worker](#deps--serialise-into-the-worker)
|
|
41
|
-
- [`proxy` — stay on the main thread, call via IPC](#proxy--stay-on-the-main-thread-call-via-ipc)
|
|
42
|
-
- [Using both together](#using-both-together)
|
|
43
|
-
- [AbortController](#abortcontroller)
|
|
44
|
-
- [Retry and Dead Letter](#retry-and-dead-letter)
|
|
45
|
-
- [Graceful Shutdown](#graceful-shutdown)
|
|
46
|
-
- [AsyncLocalStorage Propagation](#asynclocalstorage-propagation)
|
|
47
|
-
- [OpenTelemetry Trace Propagation](#opentelemetry-trace-propagation)
|
|
48
|
-
- [Health Indicator](#health-indicator)
|
|
49
|
-
- [Metrics](#metrics)
|
|
50
|
-
- [Priority Queue](#priority-queue)
|
|
51
|
-
- [Per-Worker Concurrency](#per-worker-concurrency)
|
|
52
|
-
- [Constraints](#constraints)
|
|
53
|
-
- [Arguments and return values](#arguments-and-return-values)
|
|
54
|
-
- [Compiled output required](#compiled-output-required)
|
|
55
|
-
- [Circular deps](#circular-deps)
|
|
56
|
-
|
|
57
|
-
---
|
|
58
|
-
|
|
59
|
-
## Features
|
|
60
|
-
|
|
61
|
-
- **Worker pool** — pre-spawned threads, warmed up before the first request
|
|
62
|
-
- **Zero cold start** — pool initialises on `onModuleInit`, not on the first call
|
|
63
|
-
- **Per-worker concurrency** — opt-in pipelining (`concurrency > 1`) keeps each worker busy across awaits and short tasks
|
|
64
|
-
- **Automatic message batching** — jobs and results are coalesced into a single `postMessage` per scheduling pass, amortising `structuredClone` overhead
|
|
65
|
-
- **Priority queue** — `HIGH / NORMAL / LOW`, binary-search sorted; no jobs are ever dropped
|
|
66
|
-
- **Decorator discovery** — `@WorkerClass` + `@WorkerTask` replace all manual registration
|
|
67
|
-
- **deps** — services serialised into the worker via `vm.runInContext()` + snapshot; use for plain config/data helpers
|
|
68
|
-
- **proxy** — services that stay on the main thread; the worker calls them transparently via IPC round-trip; use for DB, HTTP, queues
|
|
69
|
-
- **Retry + dead letter** — automatic retry with configurable delay; exhausted jobs emit a `dead` event
|
|
70
|
-
- **AbortController** — cancel queued or running tasks via `AbortSignal`
|
|
71
|
-
- **Graceful shutdown** — drains in-flight jobs before terminating workers, with a configurable deadline
|
|
72
|
-
- **Structured error forwarding** — errors preserve `name`, `message`, `stack`, `code`, and custom fields across the thread boundary
|
|
73
|
-
- **AsyncLocalStorage propagation** — ALS context (request ID, tenant, user) is snapshotted and restored inside workers
|
|
74
|
-
- **OpenTelemetry trace propagation** — active span context is injected into each job; no hard dependency
|
|
75
|
-
- **Health indicator** — plugs into `@nestjs/terminus`
|
|
76
|
-
- **Metrics** — counters, per-task duration percentiles (p50/p95/p99); push to any provider
|
|
77
|
-
|
|
78
|
-
---
|
|
79
|
-
|
|
80
|
-
## Requirements
|
|
81
|
-
|
|
82
|
-
| Package
|
|
83
|
-
|
|
84
|
-
| Node.js
|
|
85
|
-
| `@nestjs/common`
|
|
86
|
-
| `@nestjs/core`
|
|
87
|
-
| `reflect-metadata`
|
|
88
|
-
| TypeScript `target` | `ES2022` or higher
|
|
89
|
-
|
|
90
|
-
> **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.
|
|
91
|
-
|
|
92
|
-
`tsconfig.json` must have:
|
|
93
|
-
|
|
94
|
-
```json
|
|
95
|
-
{
|
|
96
|
-
"compilerOptions": {
|
|
97
|
-
"target": "ES2022",
|
|
98
|
-
"experimentalDecorators": true,
|
|
99
|
-
"emitDecoratorMetadata": true
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
---
|
|
105
|
-
|
|
106
|
-
## Installation
|
|
107
|
-
|
|
108
|
-
```bash
|
|
109
|
-
npm install nestworker
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
---
|
|
113
|
-
|
|
114
|
-
## Quick Start
|
|
115
|
-
|
|
116
|
-
### 1. Register `WorkerModule`
|
|
117
|
-
|
|
118
|
-
```ts
|
|
119
|
-
// app.module.ts
|
|
120
|
-
import { Module } from '@nestjs/common';
|
|
121
|
-
import { WorkerModule } from 'nestworker';
|
|
122
|
-
|
|
123
|
-
@Module({
|
|
124
|
-
imports: [
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
import {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
|
211
|
-
|
|
212
|
-
| `
|
|
213
|
-
| `
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
```
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
```
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
```
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
@
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
import {
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
@
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
1
|
+

|
|
2
|
+

|
|
3
|
+

|
|
4
|
+

|
|
5
|
+

|
|
6
|
+

|
|
7
|
+

|
|
8
|
+

|
|
9
|
+

|
|
10
|
+

|
|
11
|
+
|
|
12
|
+
<p align="center">
|
|
13
|
+
<img src="icon.svg" width="120" alt="nestworker" />
|
|
14
|
+
</p>
|
|
15
|
+
|
|
16
|
+
# nestworker
|
|
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, retry, graceful shutdown, health checks, metrics, and transparent NestJS dependency injection inside workers.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Table of Contents
|
|
23
|
+
|
|
24
|
+
- [Features](#features)
|
|
25
|
+
- [Requirements](#requirements)
|
|
26
|
+
- [Installation](#installation)
|
|
27
|
+
- [Quick Start](#quick-start)
|
|
28
|
+
- [1. Register `WorkerModule`](#1-register-workermodule)
|
|
29
|
+
- [2. Decorate your service](#2-decorate-your-service)
|
|
30
|
+
- [3. Call `run()`](#3-call-run)
|
|
31
|
+
- [API](#api)
|
|
32
|
+
- [`WorkerModule.forRoot(options?)`](#workermoduleforrootoptions)
|
|
33
|
+
- [`WorkerModule.forRootAsync(options)`](#workermoduleforrootasyncoptions)
|
|
34
|
+
- [`@WorkerClass(options?)`](#workerclassoptions)
|
|
35
|
+
- [`@WorkerTask(options?)`](#workertaskoptions)
|
|
36
|
+
- [`WorkerService.run<T>(serviceName, methodName, args?, options?)`](#workerserviceruntservicename-methodname-args-options)
|
|
37
|
+
- [`WorkerService` events](#workerservice-events)
|
|
38
|
+
- [`WorkerService.stats()`](#workerservicestats)
|
|
39
|
+
- [`deps` vs `proxy`](#deps-vs-proxy)
|
|
40
|
+
- [`deps` — serialise into the worker](#deps--serialise-into-the-worker)
|
|
41
|
+
- [`proxy` — stay on the main thread, call via IPC](#proxy--stay-on-the-main-thread-call-via-ipc)
|
|
42
|
+
- [Using both together](#using-both-together)
|
|
43
|
+
- [AbortController](#abortcontroller)
|
|
44
|
+
- [Retry and Dead Letter](#retry-and-dead-letter)
|
|
45
|
+
- [Graceful Shutdown](#graceful-shutdown)
|
|
46
|
+
- [AsyncLocalStorage Propagation](#asynclocalstorage-propagation)
|
|
47
|
+
- [OpenTelemetry Trace Propagation](#opentelemetry-trace-propagation)
|
|
48
|
+
- [Health Indicator](#health-indicator)
|
|
49
|
+
- [Metrics](#metrics)
|
|
50
|
+
- [Priority Queue](#priority-queue)
|
|
51
|
+
- [Per-Worker Concurrency](#per-worker-concurrency)
|
|
52
|
+
- [Constraints](#constraints)
|
|
53
|
+
- [Arguments and return values](#arguments-and-return-values)
|
|
54
|
+
- [Compiled output required](#compiled-output-required)
|
|
55
|
+
- [Circular deps](#circular-deps)
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Features
|
|
60
|
+
|
|
61
|
+
- **Worker pool** — pre-spawned threads, warmed up before the first request
|
|
62
|
+
- **Zero cold start** — pool initialises on `onModuleInit`, not on the first call
|
|
63
|
+
- **Per-worker concurrency** — opt-in pipelining (`concurrency > 1`) keeps each worker busy across awaits and short tasks
|
|
64
|
+
- **Automatic message batching** — jobs and results are coalesced into a single `postMessage` per scheduling pass, amortising `structuredClone` overhead
|
|
65
|
+
- **Priority queue** — `HIGH / NORMAL / LOW`, binary-search sorted; no jobs are ever dropped
|
|
66
|
+
- **Decorator discovery** — `@WorkerClass` + `@WorkerTask` replace all manual registration
|
|
67
|
+
- **deps** — services serialised into the worker via `vm.runInContext()` + snapshot; use for plain config/data helpers
|
|
68
|
+
- **proxy** — services that stay on the main thread; the worker calls them transparently via IPC round-trip; use for DB, HTTP, queues
|
|
69
|
+
- **Retry + dead letter** — automatic retry with configurable delay; exhausted jobs emit a `dead` event
|
|
70
|
+
- **AbortController** — cancel queued or running tasks via `AbortSignal`
|
|
71
|
+
- **Graceful shutdown** — drains in-flight jobs before terminating workers, with a configurable deadline
|
|
72
|
+
- **Structured error forwarding** — errors preserve `name`, `message`, `stack`, `code`, and custom fields across the thread boundary
|
|
73
|
+
- **AsyncLocalStorage propagation** — ALS context (request ID, tenant, user) is snapshotted and restored inside workers
|
|
74
|
+
- **OpenTelemetry trace propagation** — active span context is injected into each job; no hard dependency
|
|
75
|
+
- **Health indicator** — plugs into `@nestjs/terminus`
|
|
76
|
+
- **Metrics** — counters, per-task duration percentiles (p50/p95/p99); push to any provider
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Requirements
|
|
81
|
+
|
|
82
|
+
| Package | Version |
|
|
83
|
+
| ------------------- | ----------------------------------------------------------------- |
|
|
84
|
+
| Node.js | ≥ 18 (uses the global `structuredClone`, available since Node 17) |
|
|
85
|
+
| `@nestjs/common` | `^10` or `^11` |
|
|
86
|
+
| `@nestjs/core` | `^10` or `^11` |
|
|
87
|
+
| `reflect-metadata` | `^0.1` or `^0.2` |
|
|
88
|
+
| TypeScript `target` | `ES2022` or higher |
|
|
89
|
+
|
|
90
|
+
> **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.
|
|
91
|
+
|
|
92
|
+
`tsconfig.json` must have:
|
|
93
|
+
|
|
94
|
+
```json
|
|
95
|
+
{
|
|
96
|
+
"compilerOptions": {
|
|
97
|
+
"target": "ES2022",
|
|
98
|
+
"experimentalDecorators": true,
|
|
99
|
+
"emitDecoratorMetadata": true
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Installation
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
npm install nestworker
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## Quick Start
|
|
115
|
+
|
|
116
|
+
### 1. Register `WorkerModule`
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
// app.module.ts
|
|
120
|
+
import { Module } from '@nestjs/common';
|
|
121
|
+
import { WorkerModule } from 'nestworker';
|
|
122
|
+
|
|
123
|
+
@Module({
|
|
124
|
+
imports: [WorkerModule.forRoot({ poolSize: 4 })],
|
|
125
|
+
})
|
|
126
|
+
export class AppModule {}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Or async, when options come from `ConfigService`:
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
WorkerModule.forRootAsync({
|
|
133
|
+
inject: [ConfigService],
|
|
134
|
+
useFactory: (cfg: ConfigService) => ({
|
|
135
|
+
poolSize: cfg.get<number>('WORKER_POOL_SIZE'),
|
|
136
|
+
shutdownTimeout: 30_000,
|
|
137
|
+
}),
|
|
138
|
+
});
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### 2. Decorate your service
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
// image.service.ts
|
|
145
|
+
import { Injectable } from '@nestjs/common';
|
|
146
|
+
import { WorkerClass, WorkerTask } from 'nestworker';
|
|
147
|
+
import { ConfigService } from './config.service';
|
|
148
|
+
|
|
149
|
+
@Injectable()
|
|
150
|
+
@WorkerClass({ deps: [ConfigService] })
|
|
151
|
+
export class ImageService {
|
|
152
|
+
constructor(private readonly configService: ConfigService) {}
|
|
153
|
+
|
|
154
|
+
@WorkerTask({ priority: 'HIGH', timeout: 10_000, retry: 2, retryDelay: 500 })
|
|
155
|
+
resizeImage(value: number): number {
|
|
156
|
+
const multiplier = this.configService.getNumber('MULTIPLIER');
|
|
157
|
+
let total = 0;
|
|
158
|
+
for (let i = 0; i < 10_000_000; i++) total += i * value * multiplier;
|
|
159
|
+
return total;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### 3. Call `run()`
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
// image.controller.ts
|
|
168
|
+
import { Controller, Get } from '@nestjs/common';
|
|
169
|
+
import { WorkerService } from 'nestworker';
|
|
170
|
+
|
|
171
|
+
@Controller('images')
|
|
172
|
+
export class ImageController {
|
|
173
|
+
constructor(private readonly workerService: WorkerService) {}
|
|
174
|
+
|
|
175
|
+
@Get('resize')
|
|
176
|
+
resize() {
|
|
177
|
+
return this.workerService.run<number>('ImageService', 'resizeImage', [5]);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Typed invocation
|
|
183
|
+
|
|
184
|
+
`run(serviceName, methodName, args)` is convenient but stringly-typed. For
|
|
185
|
+
compile-time safety on both the method name and its argument shape, use
|
|
186
|
+
`invoke(Class)` — calling any method on the returned handle delegates to
|
|
187
|
+
`run` and infers the right return type:
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
import { ImageService } from './image.service';
|
|
191
|
+
|
|
192
|
+
const out = await this.workerService.invoke(ImageService).resizeImage(5);
|
|
193
|
+
// ^? number
|
|
194
|
+
|
|
195
|
+
// Per-invocation options (priority, timeout, signal, …):
|
|
196
|
+
await this.workerService
|
|
197
|
+
.invoke(ImageService, { timeout: 5_000 })
|
|
198
|
+
.generateThumbnail(320, 240);
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## API
|
|
204
|
+
|
|
205
|
+
### `WorkerModule.forRoot(options?)`
|
|
206
|
+
|
|
207
|
+
| Option | Type | Default | Description |
|
|
208
|
+
| -------------------- | ------------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
209
|
+
| `poolSize` | `number` | `os.cpus().length` | Worker thread count |
|
|
210
|
+
| `concurrency` | `number` | `1` | Max in-flight jobs **per worker**. Set `> 1` to pipeline jobs so workers don't sit idle between results, or while a task is awaiting I/O (proxy IPC, `fetch`, `fs`, …). Keep at `1` for purely CPU-bound, fully blocking tasks. |
|
|
211
|
+
| `shutdownTimeout` | `number` | `30_000` | Ms to wait for in-flight jobs on shutdown |
|
|
212
|
+
| `maxQueueDepth` | `number` | `Infinity` | Reject new tasks with `QueueFullError` once the pending queue exceeds this size (backpressure) |
|
|
213
|
+
| `logger` | `{ error, warn, debug? }` | NestJS `Logger` | Plug pino / winston / etc. — anything with `error(msg, trace?)` / `warn(msg)` works |
|
|
214
|
+
| `asyncLocalStorages` | `AsyncLocalStorage[]` | `[]` | ALS instances to propagate into workers |
|
|
215
|
+
|
|
216
|
+
> **`concurrency` ⚠ shared-state footgun.** Pipelined jobs share the same
|
|
217
|
+
> service instance inside a worker. If a `@WorkerTask` mutates instance
|
|
218
|
+
> state (counters, caches keyed without a request id, …), interleaved jobs
|
|
219
|
+
> will trample each other. Keep workers stateless or scope mutable state by
|
|
220
|
+
> jobId. Stateless transforms are safe.
|
|
221
|
+
|
|
222
|
+
> **`maxQueueDepth` + `stats().saturation`.** Read `ws.stats().saturation`
|
|
223
|
+
> (0–1) periodically to drive autoscaling or shed load before
|
|
224
|
+
> `QueueFullError` starts firing.
|
|
225
|
+
|
|
226
|
+
### `WorkerModule.forRootAsync(options)`
|
|
227
|
+
|
|
228
|
+
| Field | Type | Description |
|
|
229
|
+
| ------------ | ---------------------------------- | ---------------------------------- |
|
|
230
|
+
| `inject` | `any[]` | Tokens to inject into `useFactory` |
|
|
231
|
+
| `useFactory` | `(...args) => WorkerModuleOptions` | Factory — may be async |
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
### `@WorkerClass(options?)`
|
|
236
|
+
|
|
237
|
+
Marks a NestJS provider as a container of worker tasks.
|
|
238
|
+
|
|
239
|
+
| Option | Type | Description |
|
|
240
|
+
| ------- | -------- | ------------------------------------------------------------------------------------------------------------------- |
|
|
241
|
+
| `deps` | `Type[]` | Services to **serialise** into the worker. Must hold plain cloneable data — no DB connections, sockets, or streams. |
|
|
242
|
+
| `proxy` | `Type[]` | Services that **stay on the main thread**. The worker calls them via IPC. Use for anything with I/O. |
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
### `@WorkerTask(options?)`
|
|
247
|
+
|
|
248
|
+
Marks a method to be offloaded to a worker thread.
|
|
249
|
+
|
|
250
|
+
| Option | Type | Default | Description |
|
|
251
|
+
| ------------ | --------------------------------------- | ---------- | ------------------------------------------ |
|
|
252
|
+
| `priority` | `'HIGH' \| 'NORMAL' \| 'LOW'` | `'NORMAL'` | Queue priority |
|
|
253
|
+
| `timeout` | `number` | — | Reject after this many ms |
|
|
254
|
+
| `retry` | `number` | `0` | Extra attempts after first failure |
|
|
255
|
+
| `retryDelay` | `number \| (attempt: number) => number` | `0` | Ms between retry attempts. See note below. |
|
|
256
|
+
|
|
257
|
+
> **`retryDelay` as a function:** functions can't cross the worker boundary, so when a function is supplied it's evaluated on the main thread at discovery time as the average of `fn(1)`, `fn(2)`, `fn(3)` and a warning is logged. For precise control (including exponential backoff) pass a number and recreate the curve with the per-call `RunOptions.retryDelay` override.
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
### `WorkerService.run<T>(serviceName, methodName, args?, options?)`
|
|
262
|
+
|
|
263
|
+
| Parameter | Type | Description |
|
|
264
|
+
| ------------- | ------------ | ----------------------------------------- |
|
|
265
|
+
| `serviceName` | `string` | Class name of the `@WorkerClass` provider |
|
|
266
|
+
| `methodName` | `string` | Method decorated with `@WorkerTask` |
|
|
267
|
+
| `args` | `unknown[]` | structuredClone-compatible arguments |
|
|
268
|
+
| `options` | `RunOptions` | Per-call overrides (see below) |
|
|
269
|
+
|
|
270
|
+
```ts
|
|
271
|
+
interface RunOptions {
|
|
272
|
+
priority?: TaskPriority;
|
|
273
|
+
timeout?: number;
|
|
274
|
+
retry?: number;
|
|
275
|
+
retryDelay?: number;
|
|
276
|
+
signal?: AbortSignal; // cancel the task
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
### `WorkerService` events
|
|
283
|
+
|
|
284
|
+
```ts
|
|
285
|
+
workerService.onTaskStart((job) => { ... });
|
|
286
|
+
workerService.onTaskEnd((job, durationMs) => { ... });
|
|
287
|
+
workerService.onTaskError((job, error) => { ... });
|
|
288
|
+
workerService.onDead((event) => { ... }); // job exhausted all retries
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
### `WorkerService.stats()`
|
|
294
|
+
|
|
295
|
+
Returns a point-in-time snapshot of the pool — used by the health indicator and metrics service, but also useful on its own:
|
|
296
|
+
|
|
297
|
+
```ts
|
|
298
|
+
const { poolSize, idle, busy, queued, warmingUp } = workerService.stats();
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
```ts
|
|
302
|
+
interface PoolStats {
|
|
303
|
+
poolSize: number;
|
|
304
|
+
idle: number;
|
|
305
|
+
busy: number;
|
|
306
|
+
queued: number;
|
|
307
|
+
warmingUp: number;
|
|
308
|
+
}
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
---
|
|
312
|
+
|
|
313
|
+
## `deps` vs `proxy`
|
|
314
|
+
|
|
315
|
+
This is the most important decision when declaring a `@WorkerClass`.
|
|
316
|
+
|
|
317
|
+
### `deps` — serialise into the worker
|
|
318
|
+
|
|
319
|
+
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.
|
|
320
|
+
|
|
321
|
+
**Use when:** the service holds plain data (config values, lookup tables, constants) and its methods are pure computation over that data.
|
|
322
|
+
|
|
323
|
+
```ts
|
|
324
|
+
// ConfigService holds { multiplier: 3, iterations: 1_000_000 }
|
|
325
|
+
// — plain object, fully cloneable → safe to use as dep
|
|
326
|
+
|
|
327
|
+
@WorkerClass({ deps: [ConfigService] })
|
|
328
|
+
export class ImageService {
|
|
329
|
+
constructor(private readonly configService: ConfigService) {}
|
|
330
|
+
|
|
331
|
+
@WorkerTask()
|
|
332
|
+
resize(value: number): number {
|
|
333
|
+
// configService is a local copy inside the worker — no IPC
|
|
334
|
+
return this.configService.getNumber('MULTIPLIER') * value;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
✅ Plain objects, arrays, primitives, `Map`, `Set`
|
|
340
|
+
❌ DB connections, HTTP clients, sockets, streams, open file handles
|
|
341
|
+
|
|
342
|
+
### `proxy` — stay on the main thread, call via IPC
|
|
343
|
+
|
|
344
|
+
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.
|
|
345
|
+
|
|
346
|
+
**Use when:** the service does I/O — database queries, HTTP calls, cache reads, queue operations.
|
|
347
|
+
|
|
348
|
+
```ts
|
|
349
|
+
// UserService queries a database — cannot be cloned → use proxy
|
|
350
|
+
|
|
351
|
+
@WorkerClass({ proxy: [UserService] })
|
|
352
|
+
export class ReportService {
|
|
353
|
+
constructor(private readonly userService: UserService) {}
|
|
354
|
+
|
|
355
|
+
@WorkerTask()
|
|
356
|
+
async generateReport(userId: string): Promise<string> {
|
|
357
|
+
// this call transparently round-trips to the main thread
|
|
358
|
+
const user = await this.userService.findById(userId);
|
|
359
|
+
|
|
360
|
+
// heavy CPU work runs in the worker
|
|
361
|
+
return crunchNumbers(user);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
The IPC round-trip looks like this:
|
|
367
|
+
|
|
368
|
+
```
|
|
369
|
+
WORKER MAIN THREAD
|
|
370
|
+
────────────────────────────────────── ───────────────────────────────
|
|
371
|
+
this.userService.findById(userId)
|
|
372
|
+
│
|
|
373
|
+
├─ postMessage({ type: 'ipc:invoke', → onMessage handler
|
|
374
|
+
│ method: 'findById', args: [...] }) │
|
|
375
|
+
│ ├─ userService.findById(userId)
|
|
376
|
+
│ │ (real DB query, main thread)
|
|
377
|
+
│ │
|
|
378
|
+
◀── postMessage({ type: 'ipc:result', ─── └─ reply with result
|
|
379
|
+
data: { id, name, ... } })
|
|
380
|
+
│
|
|
381
|
+
└─ Promise resolves with user ✓
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
> **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.
|
|
385
|
+
|
|
386
|
+
### Using both together
|
|
387
|
+
|
|
388
|
+
`deps` and `proxy` can be combined in the same `@WorkerClass`:
|
|
389
|
+
|
|
390
|
+
```ts
|
|
391
|
+
@WorkerClass({
|
|
392
|
+
deps: [ConfigService], // cloned into worker — fast local access
|
|
393
|
+
proxy: [UserService], // stays on main thread — IPC on each call
|
|
394
|
+
})
|
|
395
|
+
export class ReportService {
|
|
396
|
+
constructor(
|
|
397
|
+
private readonly configService: ConfigService,
|
|
398
|
+
private readonly userService: UserService,
|
|
399
|
+
) {}
|
|
400
|
+
|
|
401
|
+
@WorkerTask({ priority: 'LOW' })
|
|
402
|
+
async buildReport(userId: string): Promise<Buffer> {
|
|
403
|
+
const limit = this.configService.getNumber('REPORT_LIMIT'); // local, zero IPC
|
|
404
|
+
const user = await this.userService.findById(userId); // IPC round-trip
|
|
405
|
+
return heavyPdfGeneration(user, limit);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
---
|
|
411
|
+
|
|
412
|
+
## AbortController
|
|
413
|
+
|
|
414
|
+
Cancel a queued or running task by passing an `AbortSignal`:
|
|
415
|
+
|
|
416
|
+
```ts
|
|
417
|
+
const controller = new AbortController();
|
|
418
|
+
|
|
419
|
+
// Cancel after 3 seconds if not done
|
|
420
|
+
setTimeout(() => controller.abort(), 3000);
|
|
421
|
+
|
|
422
|
+
try {
|
|
423
|
+
const result = await workerService.run('ImageService', 'resizeImage', [5], {
|
|
424
|
+
signal: controller.signal,
|
|
425
|
+
});
|
|
426
|
+
} catch (err) {
|
|
427
|
+
if (err.name === 'AbortError') {
|
|
428
|
+
console.log('Task was cancelled');
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
The `AbortSignal` is also injected as the last argument of the task method, so you can respond to cancellation inside the worker:
|
|
434
|
+
|
|
435
|
+
```ts
|
|
436
|
+
@WorkerTask()
|
|
437
|
+
processChunks(data: number[], signal: AbortSignal): number {
|
|
438
|
+
let total = 0;
|
|
439
|
+
for (const chunk of data) {
|
|
440
|
+
if (signal.aborted) break; // stop early on cancel
|
|
441
|
+
total += heavyCompute(chunk);
|
|
442
|
+
}
|
|
443
|
+
return total;
|
|
444
|
+
}
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
---
|
|
448
|
+
|
|
449
|
+
## Retry and Dead Letter
|
|
450
|
+
|
|
451
|
+
```ts
|
|
452
|
+
@WorkerTask({ retry: 3, retryDelay: 1000 })
|
|
453
|
+
async fetchAndProcess(id: string): Promise<string> { ... }
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
After all attempts fail, a `dead` event fires:
|
|
457
|
+
|
|
458
|
+
```ts
|
|
459
|
+
workerService.onDead((event) => {
|
|
460
|
+
console.error(`Job ${event.jobId} failed after ${event.attempts} attempts`);
|
|
461
|
+
console.error(event.error.message);
|
|
462
|
+
// push to external DLQ, alert, etc.
|
|
463
|
+
});
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
---
|
|
467
|
+
|
|
468
|
+
## Graceful Shutdown
|
|
469
|
+
|
|
470
|
+
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.
|
|
471
|
+
|
|
472
|
+
```ts
|
|
473
|
+
WorkerModule.forRoot({ shutdownTimeout: 30_000 });
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
---
|
|
477
|
+
|
|
478
|
+
## AsyncLocalStorage Propagation
|
|
479
|
+
|
|
480
|
+
Pass your ALS instances to `forRoot` — their current store is snapshotted at dispatch time and restored inside the worker before the task runs:
|
|
481
|
+
|
|
482
|
+
```ts
|
|
483
|
+
export const requestAls = new AsyncLocalStorage<{ requestId: string }>();
|
|
484
|
+
|
|
485
|
+
WorkerModule.forRoot({
|
|
486
|
+
asyncLocalStorages: [requestAls],
|
|
487
|
+
})
|
|
488
|
+
|
|
489
|
+
// Inside a worker task:
|
|
490
|
+
@WorkerTask()
|
|
491
|
+
process(): void {
|
|
492
|
+
const store = requestAls.getStore(); // { requestId: '...' } ✓
|
|
493
|
+
}
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
---
|
|
497
|
+
|
|
498
|
+
## OpenTelemetry Trace Propagation
|
|
499
|
+
|
|
500
|
+
If `@opentelemetry/api` is installed in your app, nestworker captures the active span context on every `run()` and re-activates it inside the worker before the task runs — distributed traces stay continuous across the thread boundary. There is **no hard dependency**: the lookup is a one-shot cached `require()` and silently no-ops when the package isn't present.
|
|
501
|
+
|
|
502
|
+
```bash
|
|
503
|
+
npm install @opentelemetry/api
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
```ts
|
|
507
|
+
// Spans created inside @WorkerTask methods will be children of the
|
|
508
|
+
// active span on the main thread at the moment run() was called.
|
|
509
|
+
@WorkerTask()
|
|
510
|
+
async heavyWork(): Promise<void> {
|
|
511
|
+
const tracer = trace.getTracer('my-app');
|
|
512
|
+
await tracer.startActiveSpan('heavy-work', async (span) => {
|
|
513
|
+
// ...
|
|
514
|
+
span.end();
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
---
|
|
520
|
+
|
|
521
|
+
## Health Indicator
|
|
522
|
+
|
|
523
|
+
```ts
|
|
524
|
+
// health.module.ts
|
|
525
|
+
import { WorkerHealthIndicator } from 'nestworker';
|
|
526
|
+
|
|
527
|
+
@Module({ providers: [WorkerHealthIndicator] })
|
|
528
|
+
export class HealthModule {}
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
```ts
|
|
532
|
+
// health.controller.ts
|
|
533
|
+
import { Controller, Get } from '@nestjs/common';
|
|
534
|
+
import { HealthCheck, HealthCheckService } from '@nestjs/terminus';
|
|
535
|
+
import { WorkerHealthIndicator } from 'nestworker';
|
|
536
|
+
|
|
537
|
+
@Controller('health')
|
|
538
|
+
export class HealthController {
|
|
539
|
+
constructor(
|
|
540
|
+
private readonly health: HealthCheckService,
|
|
541
|
+
private readonly workerHealth: WorkerHealthIndicator,
|
|
542
|
+
) {}
|
|
543
|
+
|
|
544
|
+
@Get()
|
|
545
|
+
@HealthCheck()
|
|
546
|
+
check() {
|
|
547
|
+
return this.health.check([() => this.workerHealth.check('workers')]);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
Reports `down` when workers are still warming up or queue depth exceeds pool size.
|
|
553
|
+
|
|
554
|
+
---
|
|
555
|
+
|
|
556
|
+
## Metrics
|
|
557
|
+
|
|
558
|
+
```ts
|
|
559
|
+
// app.module.ts
|
|
560
|
+
import { WorkerMetricsService } from 'nestworker';
|
|
561
|
+
|
|
562
|
+
@Module({ providers: [WorkerMetricsService] })
|
|
563
|
+
export class AppModule {}
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
```ts
|
|
567
|
+
// metrics.controller.ts
|
|
568
|
+
import { WorkerMetricsService } from 'nestworker';
|
|
569
|
+
|
|
570
|
+
@Controller('metrics')
|
|
571
|
+
export class MetricsController {
|
|
572
|
+
constructor(private readonly workerMetrics: WorkerMetricsService) {}
|
|
573
|
+
|
|
574
|
+
@Get()
|
|
575
|
+
snapshot() {
|
|
576
|
+
return this.workerMetrics.snapshot();
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
```json
|
|
582
|
+
{
|
|
583
|
+
"jobsTotal": 1500,
|
|
584
|
+
"jobsSuccess": 1480,
|
|
585
|
+
"jobsFailed": 15,
|
|
586
|
+
"jobsTimeout": 3,
|
|
587
|
+
"jobsDead": 2,
|
|
588
|
+
"queueDepth": 4,
|
|
589
|
+
"idleWorkers": 2,
|
|
590
|
+
"busyWorkers": 6,
|
|
591
|
+
"durations": {
|
|
592
|
+
"ImageService.resizeImage": {
|
|
593
|
+
"p50": 42,
|
|
594
|
+
"p95": 310,
|
|
595
|
+
"p99": 890,
|
|
596
|
+
"count": 1200
|
|
597
|
+
},
|
|
598
|
+
"ReportService.buildReport": {
|
|
599
|
+
"p50": 180,
|
|
600
|
+
"p95": 950,
|
|
601
|
+
"p99": 2100,
|
|
602
|
+
"count": 300
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
---
|
|
609
|
+
|
|
610
|
+
## Priority Queue
|
|
611
|
+
|
|
612
|
+
Jobs queue when all threads are busy, sorted by priority — `HIGH` always runs before `NORMAL` before `LOW`. Within the same priority, FIFO.
|
|
613
|
+
|
|
614
|
+
```ts
|
|
615
|
+
await Promise.all([
|
|
616
|
+
workerService.run('Svc', 'task', [], { priority: 'LOW' }),
|
|
617
|
+
workerService.run('Svc', 'task', [], { priority: 'HIGH' }),
|
|
618
|
+
workerService.run('Svc', 'task', [], { priority: 'NORMAL' }),
|
|
619
|
+
workerService.run('Svc', 'task', [], { priority: 'HIGH' }),
|
|
620
|
+
]);
|
|
621
|
+
// Execution order: HIGH → HIGH → NORMAL → LOW
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
---
|
|
625
|
+
|
|
626
|
+
## Per-Worker Concurrency
|
|
627
|
+
|
|
628
|
+
By default each worker processes one job at a time. When tasks are short, or
|
|
629
|
+
they `await` I/O (proxy IPC round-trips, `fetch`, `fs`, queue calls), the worker
|
|
630
|
+
sits idle while the main thread processes the previous result. Set
|
|
631
|
+
`concurrency > 1` to pipeline jobs into each worker and keep them saturated:
|
|
632
|
+
|
|
633
|
+
```ts
|
|
634
|
+
WorkerModule.forRoot({
|
|
635
|
+
poolSize: 4, // 4 worker threads
|
|
636
|
+
concurrency: 8, // up to 8 in-flight jobs per worker → 32 concurrent jobs
|
|
637
|
+
});
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
Guidance:
|
|
641
|
+
|
|
642
|
+
- **CPU-bound, fully blocking tasks** → keep at `1`. Extra concurrency cannot
|
|
643
|
+
help when the JS thread never yields.
|
|
644
|
+
- **Short tasks (sub-millisecond)** → `2–4` is usually enough to hide the
|
|
645
|
+
per-job postMessage cost.
|
|
646
|
+
- **Tasks awaiting I/O or proxy calls** → match `concurrency` to your typical
|
|
647
|
+
in-flight wait depth (e.g. `8–32`).
|
|
648
|
+
|
|
649
|
+
Internally the pool also coalesces every job it dispatches in a single
|
|
650
|
+
scheduling pass into one `postMessage` envelope per worker, and the worker
|
|
651
|
+
flushes accumulated results once per microtask tick. Batching is automatic —
|
|
652
|
+
there is nothing to configure.
|
|
653
|
+
|
|
654
|
+
---
|
|
655
|
+
|
|
656
|
+
## Constraints
|
|
657
|
+
|
|
658
|
+
### Arguments and return values
|
|
659
|
+
|
|
660
|
+
Must be [structuredClone](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm) compatible.
|
|
661
|
+
|
|
662
|
+
| ✅ Supported | ❌ Not supported |
|
|
663
|
+
| ------------------------------------- | ------------------------------ |
|
|
664
|
+
| Primitives, plain objects, arrays | Class instances with methods |
|
|
665
|
+
| `Map`, `Set`, `ArrayBuffer`, `Buffer` | Functions, closures |
|
|
666
|
+
| `TypedArray`, `DataView` | `Promise`, `WeakMap`, `Socket` |
|
|
667
|
+
|
|
668
|
+
### Compiled output required
|
|
669
|
+
|
|
670
|
+
nestworker locates class files via `require.cache`. The project must be compiled to `.js` before running — `ts-node` is not supported.
|
|
671
|
+
|
|
672
|
+
### Circular deps
|
|
673
|
+
|
|
674
|
+
Circular dependencies between `@WorkerClass({ deps })` entries are not supported.
|
|
675
|
+
|
|
676
|
+
---
|
|
677
|
+
|
|
678
|
+
## Contributing
|
|
679
|
+
|
|
680
|
+
See the [contributing guide](https://github.com/VaheHak/nestworker/blob/master/CONTRIBUTING.md).
|
|
681
|
+
|
|
682
|
+
## License
|
|
683
|
+
|
|
684
|
+
Licensed under [MIT](https://github.com/VaheHak/nestworker/blob/master/LICENSE).
|