nestworker 2.0.8 → 2.1.1

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 (34) hide show
  1. package/README.md +388 -117
  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 +288 -103
  8. package/dist/core/worker.pool.js.map +1 -1
  9. package/dist/core/worker.service.d.ts +28 -74
  10. package/dist/core/worker.service.js +132 -79
  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/discovery/discovery.service.d.ts +2 -9
  16. package/dist/discovery/discovery.service.js +86 -21
  17. package/dist/discovery/discovery.service.js.map +1 -1
  18. package/dist/example/bench.d.ts +13 -0
  19. package/dist/example/bench.js +85 -0
  20. package/dist/example/bench.js.map +1 -0
  21. package/dist/example/main.js +14 -2
  22. package/dist/example/main.js.map +1 -1
  23. package/dist/health/worker.health.d.ts +46 -0
  24. package/dist/health/worker.health.js +77 -0
  25. package/dist/health/worker.health.js.map +1 -0
  26. package/dist/index.d.ts +6 -1
  27. package/dist/index.js +10 -1
  28. package/dist/index.js.map +1 -1
  29. package/dist/metrics/worker.metrics.d.ts +65 -0
  30. package/dist/metrics/worker.metrics.js +122 -0
  31. package/dist/metrics/worker.metrics.js.map +1 -0
  32. package/dist/worker/worker-runtime.js +139 -29
  33. package/dist/worker/worker-runtime.js.map +1 -1
  34. package/package.json +2 -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 | ≥ 18 (uses the global `structuredClone`, available since Node 17) |
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,174 +136,432 @@ 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
- ### What is safe to import inside a worker
147
+ ## API
141
148
 
142
- | ✅ Safe | ❌ Not safe |
143
- |---|---|
144
- | Node built-ins: `os`, `path`, `crypto`, `zlib`, `fs` | HTTP clients (`axios`, `fetch`) |
145
- | Pure computation libraries | Database drivers |
146
- | `Buffer`, `Math`, `Date` | `Socket`, `Stream` |
149
+ ### `WorkerModule.forRoot(options?)`
150
+
151
+ | Option | Type | Default | Description |
152
+ |---|---|---|---|
153
+ | `poolSize` | `number` | `os.cpus().length` | Worker thread count |
154
+ | `shutdownTimeout` | `number` | `30_000` | Ms to wait for in-flight jobs on shutdown |
155
+ | `asyncLocalStorages` | `AsyncLocalStorage[]` | `[]` | ALS instances to propagate into workers |
156
+
157
+ ### `WorkerModule.forRootAsync(options)`
158
+
159
+ | Field | Type | Description |
160
+ |---|---|---|
161
+ | `inject` | `any[]` | Tokens to inject into `useFactory` |
162
+ | `useFactory` | `(...args) => WorkerModuleOptions` | Factory — may be async |
147
163
 
148
164
  ---
149
165
 
150
- ## API
166
+ ### `@WorkerClass(options?)`
151
167
 
152
- ### `WorkerModule.forRoot(options?)`
168
+ Marks a NestJS provider as a container of worker tasks.
153
169
 
154
- Registers the module globally. Call once at the application root.
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. |
155
174
 
156
- ```ts
157
- WorkerModule.forRoot({
158
- poolSize: 4, // default: os.cpus().length
159
- })
160
- ```
175
+ ---
176
+
177
+ ### `@WorkerTask(options?)`
178
+
179
+ Marks a method to be offloaded to a worker thread.
161
180
 
162
181
  | Option | Type | Default | Description |
163
182
  |---|---|---|---|
164
- | `poolSize` | `number` | `os.cpus().length` | Number of worker threads to spawn |
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 \| (attempt: number) => number` | `0` | Ms between retry attempts. See note below. |
187
+
188
+ > **`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.
165
189
 
166
190
  ---
167
191
 
168
- ### `@WorkerClass(options?)`
192
+ ### `WorkerService.run<T>(serviceName, methodName, args?, options?)`
169
193
 
170
- Class decorator. Marks a NestJS provider as a container of worker tasks.
194
+ | Parameter | Type | Description |
195
+ |---|---|---|
196
+ | `serviceName` | `string` | Class name of the `@WorkerClass` provider |
197
+ | `methodName` | `string` | Method decorated with `@WorkerTask` |
198
+ | `args` | `unknown[]` | structuredClone-compatible arguments |
199
+ | `options` | `RunOptions` | Per-call overrides (see below) |
171
200
 
172
201
  ```ts
173
- @WorkerClass({ deps: [ConfigService, LoggerService] })
174
- export class MyService { ... }
202
+ interface RunOptions {
203
+ priority?: TaskPriority;
204
+ timeout?: number;
205
+ retry?: number;
206
+ retryDelay?: number;
207
+ signal?: AbortSignal; // cancel the task
208
+ }
175
209
  ```
176
210
 
177
- | Option | Type | Description |
178
- |---|---|---|
179
- | `deps` | `Type[]` | Injectable dependencies to reconstruct inside workers |
211
+ ---
212
+
213
+ ### `WorkerService` events
214
+
215
+ ```ts
216
+ workerService.onTaskStart((job) => { ... });
217
+ workerService.onTaskEnd((job, durationMs) => { ... });
218
+ workerService.onTaskError((job, error) => { ... });
219
+ workerService.onDead((event) => { ... }); // job exhausted all retries
220
+ ```
180
221
 
181
222
  ---
182
223
 
183
- ### `@WorkerTask(options?)`
224
+ ### `WorkerService.stats()`
184
225
 
185
- Method decorator. Marks a method to be offloaded to a worker thread.
226
+ Returns a point-in-time snapshot of the pool used by the health indicator and metrics service, but also useful on its own:
186
227
 
187
228
  ```ts
188
- @WorkerTask({ priority: 'HIGH', timeout: 3000 })
189
- heavyComputation(input: number): number { ... }
229
+ const { poolSize, idle, busy, queued, warmingUp } = workerService.stats();
190
230
  ```
191
231
 
192
- | Option | Type | Default | Description |
193
- |---|---|---|---|
194
- | `priority` | `'HIGH' \| 'NORMAL' \| 'LOW'` | `'NORMAL'` | Queue priority — `HIGH` jobs run first |
195
- | `timeout` | `number` | `undefined` | Reject the job after this many ms |
232
+ ```ts
233
+ interface PoolStats {
234
+ poolSize: number;
235
+ idle: number;
236
+ busy: number;
237
+ queued: number;
238
+ warmingUp: number;
239
+ }
240
+ ```
196
241
 
197
242
  ---
198
243
 
199
- ### `WorkerService.run<T>(serviceName, methodName, args, overrides?)`
244
+ ## `deps` vs `proxy`
245
+
246
+ This is the most important decision when declaring a `@WorkerClass`.
247
+
248
+ ### `deps` — serialise into the worker
200
249
 
201
- Executes a `@WorkerTask` method in a worker thread.
250
+ 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.
251
+
252
+ **Use when:** the service holds plain data (config values, lookup tables, constants) and its methods are pure computation over that data.
202
253
 
203
254
  ```ts
204
- // Uses priority/timeout from the @WorkerTask decorator
205
- const result = await workerService.run<number>('ImageService', 'resizeImage', [5]);
206
-
207
- // Override priority or timeout for a specific call
208
- const result = await workerService.run<number>(
209
- 'ImageService', 'resizeImage', [5],
210
- { priority: 'LOW', timeout: 10_000 }
211
- );
255
+ // ConfigService holds { multiplier: 3, iterations: 1_000_000 }
256
+ // plain object, fully cloneable → safe to use as dep
257
+
258
+ @WorkerClass({ deps: [ConfigService] })
259
+ export class ImageService {
260
+ constructor(private readonly configService: ConfigService) {}
261
+
262
+ @WorkerTask()
263
+ resize(value: number): number {
264
+ // configService is a local copy inside the worker — no IPC
265
+ return this.configService.getNumber('MULTIPLIER') * value;
266
+ }
267
+ }
212
268
  ```
213
269
 
214
- | Parameter | Type | Description |
215
- |---|---|---|
216
- | `serviceName` | `string` | Class name of the `@WorkerClass` provider |
217
- | `methodName` | `string` | Method name decorated with `@WorkerTask` |
218
- | `args` | `unknown[]` | Arguments to pass — must be structuredClone-compatible |
219
- | `overrides` | `object` | Optional `priority` / `timeout` override for this call |
270
+ Plain objects, arrays, primitives, `Map`, `Set`
271
+ ❌ DB connections, HTTP clients, sockets, streams, open file handles
272
+
273
+ ### `proxy` stay on the main thread, call via IPC
274
+
275
+ 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.
276
+
277
+ **Use when:** the service does I/O — database queries, HTTP calls, cache reads, queue operations.
278
+
279
+ ```ts
280
+ // UserService queries a database — cannot be cloned → use proxy
281
+
282
+ @WorkerClass({ proxy: [UserService] })
283
+ export class ReportService {
284
+ constructor(private readonly userService: UserService) {}
285
+
286
+ @WorkerTask()
287
+ async generateReport(userId: string): Promise<string> {
288
+ // this call transparently round-trips to the main thread
289
+ const user = await this.userService.findById(userId);
290
+
291
+ // heavy CPU work runs in the worker
292
+ return crunchNumbers(user);
293
+ }
294
+ }
295
+ ```
296
+
297
+ The IPC round-trip looks like this:
298
+
299
+ ```
300
+ WORKER MAIN THREAD
301
+ ────────────────────────────────────── ───────────────────────────────
302
+ this.userService.findById(userId)
303
+
304
+ ├─ postMessage({ type: 'ipc:invoke', → onMessage handler
305
+ │ method: 'findById', args: [...] }) │
306
+ │ ├─ userService.findById(userId)
307
+ │ │ (real DB query, main thread)
308
+ │ │
309
+ ◀── postMessage({ type: 'ipc:result', ─── └─ reply with result
310
+ data: { id, name, ... } })
311
+
312
+ └─ Promise resolves with user ✓
313
+ ```
314
+
315
+ > **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.
316
+
317
+ ### Using both together
220
318
 
221
- Returns a `Promise<T>` that resolves with the method's return value.
319
+ `deps` and `proxy` can be combined in the same `@WorkerClass`:
320
+
321
+ ```ts
322
+ @WorkerClass({
323
+ deps: [ConfigService], // cloned into worker — fast local access
324
+ proxy: [UserService], // stays on main thread — IPC on each call
325
+ })
326
+ export class ReportService {
327
+ constructor(
328
+ private readonly configService: ConfigService,
329
+ private readonly userService: UserService,
330
+ ) {}
331
+
332
+ @WorkerTask({ priority: 'LOW' })
333
+ async buildReport(userId: string): Promise<Buffer> {
334
+ const limit = this.configService.getNumber('REPORT_LIMIT'); // local, zero IPC
335
+ const user = await this.userService.findById(userId); // IPC round-trip
336
+ return heavyPdfGeneration(user, limit);
337
+ }
338
+ }
339
+ ```
222
340
 
223
341
  ---
224
342
 
225
- ## How DI in Workers Works
343
+ ## AbortController
226
344
 
227
- Worker threads run in an isolated V8 context they share no heap with the main thread. Passing live NestJS services across the boundary is not possible directly.
345
+ Cancel a queued or running task by passing an `AbortSignal`:
228
346
 
229
- nestworker solves it in three steps:
347
+ ```ts
348
+ const controller = new AbortController();
349
+
350
+ // Cancel after 3 seconds if not done
351
+ setTimeout(() => controller.abort(), 3000);
352
+
353
+ try {
354
+ const result = await workerService.run(
355
+ 'ImageService', 'resizeImage', [5],
356
+ { signal: controller.signal },
357
+ );
358
+ } catch (err) {
359
+ if (err.name === 'AbortError') {
360
+ console.log('Task was cancelled');
361
+ }
362
+ }
363
+ ```
230
364
 
231
- **1. Main thread locate compiled files**
365
+ The `AbortSignal` is also injected as the last argument of the task method, so you can respond to cancellation inside the worker:
232
366
 
233
- `serializeForWorker()` walks `require.cache` to find the compiled `.js` file path for each dep constructor. It also snapshots each dep's own properties via `structuredClone` to capture runtime state (e.g. loaded config values).
367
+ ```ts
368
+ @WorkerTask()
369
+ processChunks(data: number[], signal: AbortSignal): number {
370
+ let total = 0;
371
+ for (const chunk of data) {
372
+ if (signal.aborted) break; // stop early on cancel
373
+ total += heavyCompute(chunk);
374
+ }
375
+ return total;
376
+ }
377
+ ```
378
+
379
+ ---
234
380
 
235
- **2. Worker thread `vm.runInContext()`**
381
+ ## Retry and Dead Letter
236
382
 
237
- Each compiled `.js` file is executed inside a `vm` context with a custom `require()` that stubs NestJS decorator packages (`@nestjs/common`, `@nestjs/core`, etc.) so that decorator calls at file-eval time are silent no-ops. All other imports resolve normally through Node's module system — including Node built-ins and third-party packages.
383
+ ```ts
384
+ @WorkerTask({ retry: 3, retryDelay: 1000 })
385
+ async fetchAndProcess(id: string): Promise<string> { ... }
386
+ ```
387
+
388
+ After all attempts fail, a `dead` event fires:
389
+
390
+ ```ts
391
+ workerService.onDead((event) => {
392
+ console.error(`Job ${event.jobId} failed after ${event.attempts} attempts`);
393
+ console.error(event.error.message);
394
+ // push to external DLQ, alert, etc.
395
+ });
396
+ ```
238
397
 
239
- Each dep is reconstructed as `Object.create(DepClass.prototype)` + `Object.assign(snapshot)`, restoring both prototype methods and runtime state. The dep is then assigned to the service instance by property key.
398
+ ---
240
399
 
241
- **3. Result**
400
+ ## Graceful Shutdown
242
401
 
243
- `this.configService.get('KEY')` inside a worker task works exactly as on the main thread.
402
+ 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.
244
403
 
404
+ ```ts
405
+ WorkerModule.forRoot({ shutdownTimeout: 30_000 })
245
406
  ```
246
- MAIN THREAD WORKER THREAD
247
- ───────────────────────────────────── ──────────────────────────────────────
248
- serializeForWorker()
249
- ConfigService filePath + snapshot → vm.runInContext(config.service.js)
250
- Object.create(ConfigService.prototype)
251
- Object.assign(inst, snapshot)
252
- ImageService → filePath → vm.runInContext(image.service.js)
253
- Object.create(ImageService.prototype)
254
- inst.configService = configInst
255
- this.configService.get() ✓
407
+
408
+ ---
409
+
410
+ ## AsyncLocalStorage Propagation
411
+
412
+ Pass your ALS instances to `forRoot` — their current store is snapshotted at dispatch time and restored inside the worker before the task runs:
413
+
414
+ ```ts
415
+ export const requestAls = new AsyncLocalStorage<{ requestId: string }>();
416
+
417
+ WorkerModule.forRoot({
418
+ asyncLocalStorages: [requestAls],
419
+ })
420
+
421
+ // Inside a worker task:
422
+ @WorkerTask()
423
+ process(): void {
424
+ const store = requestAls.getStore(); // { requestId: '...' } ✓
425
+ }
256
426
  ```
257
427
 
258
- ### What deps can be used
428
+ ---
429
+
430
+ ## OpenTelemetry Trace Propagation
259
431
 
260
- Services holding plain config data (`object`, `Map`, arrays, primitives)
261
- ✅ Services whose methods only read from their own snapshotted properties
262
- ❌ Services that hold DB connections, HTTP clients, or open streams
263
- Services with `Socket`, `Stream`, or other non-cloneable properties
432
+ 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.
433
+
434
+ ```bash
435
+ npm install @opentelemetry/api
436
+ ```
437
+
438
+ ```ts
439
+ // Spans created inside @WorkerTask methods will be children of the
440
+ // active span on the main thread at the moment run() was called.
441
+ @WorkerTask()
442
+ async heavyWork(): Promise<void> {
443
+ const tracer = trace.getTracer('my-app');
444
+ await tracer.startActiveSpan('heavy-work', async (span) => {
445
+ // ...
446
+ span.end();
447
+ });
448
+ }
449
+ ```
450
+
451
+ ---
452
+
453
+ ## Health Indicator
454
+
455
+ ```ts
456
+ // health.module.ts
457
+ import { WorkerHealthIndicator } from 'nestworker';
458
+
459
+ @Module({ providers: [WorkerHealthIndicator] })
460
+ export class HealthModule {}
461
+ ```
462
+
463
+ ```ts
464
+ // health.controller.ts
465
+ import { Controller, Get } from '@nestjs/common';
466
+ import { HealthCheck, HealthCheckService } from '@nestjs/terminus';
467
+ import { WorkerHealthIndicator } from 'nestworker';
468
+
469
+ @Controller('health')
470
+ export class HealthController {
471
+ constructor(
472
+ private readonly health: HealthCheckService,
473
+ private readonly workerHealth: WorkerHealthIndicator,
474
+ ) {}
475
+
476
+ @Get()
477
+ @HealthCheck()
478
+ check() {
479
+ return this.health.check([
480
+ () => this.workerHealth.check('workers'),
481
+ ]);
482
+ }
483
+ }
484
+ ```
485
+
486
+ Reports `down` when workers are still warming up or queue depth exceeds pool size.
487
+
488
+ ---
489
+
490
+ ## Metrics
491
+
492
+ ```ts
493
+ // app.module.ts
494
+ import { WorkerMetricsService } from 'nestworker';
495
+
496
+ @Module({ providers: [WorkerMetricsService] })
497
+ export class AppModule {}
498
+ ```
499
+
500
+ ```ts
501
+ // metrics.controller.ts
502
+ import { WorkerMetricsService } from 'nestworker';
503
+
504
+ @Controller('metrics')
505
+ export class MetricsController {
506
+ constructor(private readonly workerMetrics: WorkerMetricsService) {}
507
+
508
+ @Get()
509
+ snapshot() {
510
+ return this.workerMetrics.snapshot();
511
+ }
512
+ }
513
+ ```
514
+
515
+ ```json
516
+ {
517
+ "jobsTotal": 1500,
518
+ "jobsSuccess": 1480,
519
+ "jobsFailed": 15,
520
+ "jobsTimeout": 3,
521
+ "jobsDead": 2,
522
+ "queueDepth": 4,
523
+ "idleWorkers": 2,
524
+ "busyWorkers": 6,
525
+ "durations": {
526
+ "ImageService.resizeImage": { "p50": 42, "p95": 310, "p99": 890, "count": 1200 },
527
+ "ReportService.buildReport": { "p50": 180, "p95": 950, "p99": 2100, "count": 300 }
528
+ }
529
+ }
530
+ ```
264
531
 
265
532
  ---
266
533
 
267
534
  ## Priority Queue
268
535
 
269
- 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.
536
+ Jobs queue when all threads are busy, sorted by priority — `HIGH` always runs before `NORMAL` before `LOW`. Within the same priority, FIFO.
270
537
 
271
538
  ```ts
272
- // These four tasks are dispatched to the pool concurrently.
273
- // HIGH tasks run first regardless of arrival order.
274
539
  await Promise.all([
275
- workerService.run('Svc', 'lowPriorityTask', [], { priority: 'LOW' }),
276
- workerService.run('Svc', 'highPriorityTask', [], { priority: 'HIGH' }),
277
- workerService.run('Svc', 'normalPriorityTask', [], { priority: 'NORMAL' }),
278
- workerService.run('Svc', 'highPriorityTask2', [], { priority: 'HIGH' }),
540
+ workerService.run('Svc', 'task', [], { priority: 'LOW' }),
541
+ workerService.run('Svc', 'task', [], { priority: 'HIGH' }),
542
+ workerService.run('Svc', 'task', [], { priority: 'NORMAL' }),
543
+ workerService.run('Svc', 'task', [], { priority: 'HIGH' }),
279
544
  ]);
545
+ // Execution order: HIGH → HIGH → NORMAL → LOW
280
546
  ```
547
+
281
548
  ---
282
549
 
283
550
  ## Constraints
284
551
 
285
- ### Arguments and Return Values
552
+ ### Arguments and return values
286
553
 
287
- 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.
554
+ Must be [structuredClone](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm) compatible.
288
555
 
289
556
  | ✅ Supported | ❌ Not supported |
290
557
  |---|---|
291
- | Primitives, plain objects, arrays | Class instances |
292
- | `Map`, `Set`, `ArrayBuffer` | Functions |
293
- | `TypedArray`, `DataView` | `Promise`, `WeakMap` |
558
+ | Primitives, plain objects, arrays | Class instances with methods |
559
+ | `Map`, `Set`, `ArrayBuffer`, `Buffer` | Functions, closures |
560
+ | `TypedArray`, `DataView` | `Promise`, `WeakMap`, `Socket` |
561
+
562
+ ### Compiled output required
563
+
564
+ nestworker locates class files via `require.cache`. The project must be compiled to `.js` before running — `ts-node` is not supported.
294
565
 
295
566
  ### Circular deps
296
567
 
@@ -300,7 +571,7 @@ Circular dependencies between `@WorkerClass({ deps })` entries are not supported
300
571
 
301
572
  ## Contributing
302
573
 
303
- See the [contributing guide](https://github.com/VaheHak/nestworker/blob/master/CONTRIBUTING.md) for detailed instructions on how to get started with our project.
574
+ See the [contributing guide](https://github.com/VaheHak/nestworker/blob/master/CONTRIBUTING.md).
304
575
 
305
576
  ## License
306
577