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

backend
npm install @atolljs/core

Pool module

app.module.tsbackend
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

digest.service.tsbackend
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

incidents.controller.tsbackend
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.

digest/digest.worker.tsbackend
// 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 worker

Housing 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.jsonbackend
// nest-cli.json — opt into webpack so worker entries bundle
{
  "compilerOptions": {
    "webpack": true
  }
}

// package.json
//   "build": "nest build"
//   "dev":   "nest start --watch"

Binding API

ExportSignatureWhat it does
AtollModule.forRootforRoot({ pools? }) / forRootAsync(...)Global atoll infrastructure — validator, discovery, lifecycle. Optional pools for simple apps; feature modules prefer registerPool.
AtollModule.registerPoolregisterPool(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).
workerClientworkerClient<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).
runAtollWorkerrunAtollWorker(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.
registerAtollHandlersregisterAtollHandlers(...instances)Explicit task registration for instances created outside a worker Nest context.
worker specworker: 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:

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.