Next.js — server workers
@atolljs/node inside a Next.js app: route handlers run on Node, so a handler can own a real node:worker_threads pool — dispatch CPU-bound tasks without blocking the request thread, and read the pool's shared memory directly on the API thread for zero-dispatch responses. The client-side hooks are documented under Frontend → Next.js.
Install
npm install @atolljs/core @atolljs/nodeRoute handler — the pool owner
// src/app/api/atoll/route.ts — route handlers run on Node,
// so @atolljs/node works inside them: a pool of node:worker_threads
// workers sharing one buffer with the API thread.
import { NextResponse } from 'next/server';
import { Worker } from 'node:worker_threads';
import { createNodePool, createNodeWorker } from '@atolljs/node';
import { digestMemory, HashDigest } from './digest.contract';
export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';
// Module-scope singleton via globalThis — dev-mode HMR re-evaluates the
// module; without it every hot reload would leak worker threads.
const createPool = () =>
createNodePool({
sharedMemory: digestMemory,
poolSize: 2,
tasks: { hash: HashDigest },
// webpack/turbopack detect new Worker(new URL(...)) and emit the
// worker entry as its own chunk — the factory points at TS source.
createWorker: () => createNodeWorker(
new Worker(new URL('./atoll.worker.ts', import.meta.url)),
),
});
type DigestPool = ReturnType<typeof createPool>;
const getPool = (): DigestPool => {
const g = globalThis as { __atollDigestPool?: DigestPool };
return (g.__atollDigestPool ??= createPool());
};
export async function POST(req: Request) { // CPU-bound work, off the
const body = await req.json().catch(() => ({})); // request thread
return NextResponse.json(await getPool().hash(body?.input, body?.rounds));
}
export async function GET() { // direct shared-memory read — zero dispatch
return NextResponse.json({ jobsDone: digestMemory.jobsDone.read() });
}// src/app/api/atoll/atoll.worker.ts — the shim MUST be
// first: it binds self = parentPort before workerBootstrap wires
// INIT_MEMORY / EXECUTE_TASK onto the node:worker_threads MessagePort.
import '@atolljs/node/shim';
import '@atolljs/core/worker/workerBootstrap';
import { TaskRegistry } from '@atolljs/core';
import { createHash } from 'node:crypto';
import { digestMemory, HashDigest } from './digest.contract';
TaskRegistry.register(HashDigest, (input = 'incident-feed', rounds = 50_000) => {
// chained SHA-256 … then digestMemory.jobsDone.write(…) — the route
// handler's GET reads it back without touching the pool.
});The repo's examples/nextjs implements this pattern — it exposes POST /api/atoll ({"input","rounds"} → chained SHA-256 on a worker) and GET /api/atoll (a direct shared-memory read of jobsDone); see examples/nextjs/src/app/api/atoll/. Run it locally with npm run dev in the example (or npm run serve:all from the repo root) — there is no embedded demo here because a static docs host has no Node runtime.
Going further
The example builds three more use cases on this pattern, each with a dedicated page: a job queue whose progress counters live in shared memory, a read-model API serving a 1M-record buffer with zero dispatch, boot warmup via Next's instrumentation hook, and the custom-server topology for clustering and WebSockets.
Notes
- Requires the
nodejsruntime (the default for route handlers) —output: 'export'drops handlers entirely, so the pool needs a Node host ornext start. - Hold the pool in a module-scope singleton on
globalThis— dev-mode HMR re-evaluates the module, and without it every hot reload leaks worker threads. - No COOP/COEP needed — Node always allows
SharedArrayBuffer.