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