nestworker 2.0.4 → 2.1.0

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