NestJS
@atolljs/nestjs — the worker atoll on the server. Named pools of node:worker_threads workers share memory with the API thread, and @AtollService/@AtollTask move a service method's body into a worker — with real dependency injection on both sides.
Install
npm install @atolljs/corePool module
import { Module } from '@nestjs/common';
import { Worker } from 'node:worker_threads';
import { AtollModule } from '@atolljs/nestjs';
import { digestMemory } from './digest/digest.service';
import { DigestService } from './digest/digest.service';
// The feature module owns its worker domain — the pool registers here,
// not in AppModule. The same module is bootstrapped inside each worker
// by runAtollWorker, where the pool provider resolves to null.
@Module({
imports: [
AtollModule.registerPool({
name: 'digest',
// webpack detects new Worker(new URL(...)) and emits the entry
// as its own chunk — the config references the TS source.
worker: () => new Worker(new URL('./digest.worker.ts', import.meta.url)),
sharedMemory: digestMemory,
poolSize: 2,
}),
],
providers: [DigestService],
exports: [DigestService, AtollModule], // re-exports the pool token
})
export class DigestAtollModule {}
// app.module.ts — global infrastructure once, zero pool config at root:
// imports: [AtollModule.forRoot(), DigestAtollModule, IncidentsAtollModule]Service — the decorator picks the thread
import { Inject, Injectable } from '@nestjs/common';
import { AtollService } from '@atolljs/nestjs/decorators';
import { defineSharedMemory, field } from '@atolljs/core';
export const digestMemory = defineSharedMemory({ jobsDone: field.number() });
// Class-level: EVERY method dispatches to the 'digest' pool under
// DigestService.<method> ids. @AtollTask({ pool }) remains for
// per-method control.
@Injectable()
@AtollService({ pool: 'digest' })
export class DigestService {
constructor(@Inject(ScanTelemetry) private telemetry: ScanTelemetry) {}
async hash(input: string, rounds = 50_000) {
this.telemetry.note('hash'); // injected dep resolves inside the worker
let digest = input;
for (let i = 0; i < rounds; i++) {
digest = createHash('sha256').update(digest).digest('hex');
}
digestMemory.jobsDone.write(digestMemory.jobsDone.read() + 1);
return { hash: digest, rounds };
}
}@AtollService at class level is the service-level facade: inject the provider normally and every method dispatches — consumers stay plain DI clients with zero atoll imports. Service facades walks a full example including service→service composition.
Controller — the pool wrapped as a typed client
import { Controller, Get } from '@nestjs/common';
import { InjectAtollPool } from '@atolljs/nestjs';
import { workerClient, type WorkerPool } from '@atolljs/core';
import type { IncidentsWorker } from '@atolljs/incidents';
@Controller('api/incidents')
export class IncidentsController {
// Wrap the injected pool once — calls read like the worker's methods.
// Factory form resolves this.pool lazily (field inits run before the
// constructor's parameter-property assignment).
private readonly incidents = workerClient<IncidentsWorker>(() => this.pool);
constructor(@InjectAtollPool('incidents') private readonly pool: WorkerPool) {}
@Get('stats')
stats() {
return this.incidents.computeMetrics();
}
}Worker entry
Entries live beside the module they boot — digest/digest.worker.ts, shared/incidents.worker.ts, housed/housed.worker.ts — so every module's new URL('./x.worker.ts', ...) stays inside its own directory.
// src/digest/digest.worker.ts — lives beside the module it boots;
// bundled to its own webpack chunk. atoll-nestjs/worker is self-contained:
// its own first imports bind self = parentPort and wire
// INIT_MEMORY / EXECUTE_TASK.
import { runAtollWorker } from '@atolljs/nestjs/worker';
import { DigestAtollModule } from './digest.module';
void runAtollWorker(DigestAtollModule); // real DI inside the workerHousing a partial API inside workers
Beyond task dispatch, a route subtree can live only in workers: a dedicated message-only pool boots a real Nest app per worker — decorators, DI, and guards intact — and the main app proxies a URL prefix into it, with optional clustered and WebSocket entry points to the same routes. Housed APIs walks through the whole setup; Clustering and WebSockets cover the other two entry points, and Node.js → Gateway routing documents the underlying @atolljs/node/http machinery.
Shared-memory persistence
The analog of NestJS's Redis WebSocket adapter: @atolljs/node/redis mirrors a pool's shared-memory contract to a Redis hash via the config's persistence option — restart durability plus optional cross-process replication over pub/sub, while reads/writes stay synchronous memory ops. Persistence covers wiring, replication, and caveats.
Build
Plain nest build — webpack mode. Worker chunks need no configuration: webpack detects each new Worker(new URL('./x.worker.ts', import.meta.url)) in the pool config and compiles it as its own chunk, so the worker: factory points at the TS source — never a dist filename. atoll-nestjs/worker self-contains the node:worker_threads shim + bootstrap — worker entries are a couple of imports. The lower-level node pieces (createNodeWorker, the shim, createNodePool) live in @atolljs/node — usable in plain Node programs (Express, Fastify, Hono, Koa) with no Nest at all.
// nest-cli.json — opt into webpack so worker entries bundle
{
"compilerOptions": {
"webpack": true
}
}
// package.json
// "build": "nest build"
// "dev": "nest start --watch"Binding API
| Export | Signature | What it does |
|---|---|---|
AtollModule.forRoot | forRoot({ pools? }) / forRootAsync(...) | Global atoll infrastructure — validator, discovery, lifecycle. Optional pools for simple apps; feature modules prefer registerPool. |
AtollModule.registerPool | registerPool(config) / registerPoolAsync(...) | Bull-style module-level pool registration inside the feature module that owns the worker: name, worker, sharedMemory, poolSize. Injectable provider, terminated on module destroy. |
@AtollTask | @AtollTask(taskId | contract | { pool }) | Per-method RPC offload — calls on the API thread dispatch to the pool; the body executes inside the worker’s own Nest context. |
@AtollService | @AtollService({ pool }) / @AtollService(service, opts?) | Class-level offload — marks every method for dispatch to the pool under ClassName.method ids (the contract form binds only methods declared in a ServiceContract). |
workerClient | workerClient<WorkerDef>(runner | () => runner) | Typed Proxy over an injected pool — wrap once and call the worker’s own method names: client.seedIncidents(). |
@InjectAtollPool | @InjectAtollPool(name) | Inject a configured pool directly (first-class task methods / runTask). |
runAtollWorker | runAtollWorker(module) | Worker entry point — self-contained (shim + bootstrap inside), boots a Nest application context inside the worker and registers every @AtollTask method on its DI-resolved provider. |
registerAtollHandlers | registerAtollHandlers(...instances) | Explicit task registration for instances created outside a worker Nest context. |
worker spec | worker: path | URL | (() => Worker | NodeWorker) | Pool worker declaration — a factory may return node:worker_threads.Worker directly; it is adapted internally, keeping `new Worker(new URL(...))` webpack-detectable without adapter ceremony. |
Live example
The repo's examples/nestjs runs three pools (24-worker incidents, 2-worker digest, 2-worker housed HTTP) behind a REST API on port 3100. Start it via npm run serve:all, then:
http://localhost:3100/api/incidents/stats— worker-computed aggregateshttp://localhost:3100/api/digest/worker— the answering worker's threadId + per-worker telemetryhttp://localhost:3100/api/incidents/42— a direct shared-memory read, zero dispatchhttp://localhost:3100/api/housed/incidents/whoami— the housed worker API (see Housed APIs)
Notes
- Server-only binding — SharedArrayBuffer in Node needs no COOP/COEP headers.
- nest build (webpack/ts-loader) honors emitDecoratorMetadata; explicit @Inject/@InjectAtollPool tokens are optional but harmless.
- Each pool's workers share one buffer — only define the pool's own contracts in a worker entry's module graph.
- Args/results cross postMessage (structured clone); the shared buffer carries the large state.