@atolljs/node 0.1.3 → 0.1.5

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.
package/dist/http.d.ts ADDED
@@ -0,0 +1,212 @@
1
+ import { Server as HttpServer, type IncomingMessage, type RequestListener } from 'node:http';
2
+ import { type Server as NetServer, type Socket } from 'node:net';
3
+ /** Wire id a worker uses to announce an internally-listening HTTP port. */
4
+ export declare const HTTP_PORT = "HTTP_PORT";
5
+ export declare const SOCKET_TRANSFER_SUPPORTED: boolean;
6
+ /** Anything that exposes its workers — WorkerPool is the usual source. */
7
+ export interface WorkerSource {
8
+ readonly workers: readonly Worker[];
9
+ }
10
+ export interface HttpClusterOptions {
11
+ /** The pool whose workers serve connections (respawns picked up on read). */
12
+ pool: WorkerSource;
13
+ /** Port to listen on. Ignored when `server` is provided. */
14
+ port?: number;
15
+ /** Listen host. Ignored when `server` is provided. */
16
+ host?: string;
17
+ /**
18
+ * Bring your own listener — MUST be created with `pauseOnConnect: true`
19
+ * (e.g. `net.createServer({ pauseOnConnect: true })`) or the main thread
20
+ * may consume request bytes before the socket is transferred.
21
+ */
22
+ server?: NetServer;
23
+ /**
24
+ * Connection → worker selection. Defaults to round-robin. Receives the
25
+ * fresh `pool.workers` snapshot each call, so respawns participate.
26
+ * Returning undefined destroys the socket. {@link stickyByAddress} is a
27
+ * ready-made sticky implementation for multi-connection session flows.
28
+ */
29
+ route?: (socket: Socket, workers: readonly Worker[]) => Worker | undefined;
30
+ /** Called once the server is listening. */
31
+ onListen?: (server: NetServer) => void;
32
+ }
33
+ export interface HttpCluster {
34
+ /** The listening TCP server (the user's `server` when provided). */
35
+ server: NetServer;
36
+ /** Stop accepting; in-flight transferred connections stay with workers. */
37
+ close(): Promise<void>;
38
+ }
39
+ /**
40
+ * Main thread: cluster a pool behind one listener — accept connections and
41
+ * hand each socket to a pool worker. Pairs with the worker-side
42
+ * {@link serveHttp}:
43
+ *
44
+ * const pool = createNodePool({ worker: () => new Worker(new URL('./api.worker.js', import.meta.url)), sharedMemory, poolSize: 'auto' });
45
+ * const cluster = createHttpCluster({ pool, port: 8080 }); // null below Node 26
46
+ *
47
+ * Sockets transfer BEFORE any bytes are read — the worker's http.Server does
48
+ * all parsing. HTTPS termination is not supported here (the TLS handshake
49
+ * would run on the accept thread); terminate TLS upstream or serve plain
50
+ * HTTP behind a proxy.
51
+ *
52
+ * Returns `null` on runtimes without socket transfer (Node < 26 — see
53
+ * {@link SOCKET_TRANSFER_SUPPORTED}) after logging a one-line notice, so
54
+ * callers can write the cluster unconditionally and degrade to gateway-only
55
+ * behavior without wrapping the call in capability checks.
56
+ */
57
+ export declare function createHttpCluster(options: HttpClusterOptions): HttpCluster | null;
58
+ /**
59
+ * Sticky connection routing for {@link createHttpCluster}: every connection
60
+ * from the same client address lands on the same worker — rendezvous
61
+ * (highest-random-weight) hashing over `key + slot`, so removing a worker
62
+ * only remaps ITS clients instead of reshuffling the whole pool.
63
+ *
64
+ * A transferred socket pins its whole life to one worker, so a bare
65
+ * WebSocket connection never needs this — stickiness matters for
66
+ * multi-connection session flows: socket.io's polling→upgrade sequence,
67
+ * or an HTTP request that must precede/follow a WS connection on the same
68
+ * worker. The acceptor can't read headers or cookies, so client address
69
+ * (nginx ip_hash style) is the strongest key available — supply `key` to
70
+ * mix in more (e.g. localPort for multi-listener setups).
71
+ *
72
+ * createHttpCluster({ pool, port: 8080, route: stickyByAddress() });
73
+ */
74
+ export declare function stickyByAddress(key?: (socket: Socket) => string): (socket: Socket, workers: readonly Worker[]) => Worker | undefined;
75
+ export interface ServeHttpOptions {
76
+ /**
77
+ * Listen on an internal TCP port and announce it to the parent thread via
78
+ * a `{ type: HTTP_PORT, port }` message — for the gateway topology where
79
+ * the main thread parses HTTP and proxies to worker-owned listeners
80
+ * (works on any Node version, unlike socket transfer). `0` = ephemeral.
81
+ * The server still accepts transferred sockets either way — `listen` only
82
+ * adds the internal gateway port.
83
+ */
84
+ listen?: number;
85
+ /** Listen host; defaults to '127.0.0.1' so worker ports stay internal. */
86
+ host?: string;
87
+ }
88
+ /**
89
+ * Worker side: serve HTTP with an http.Server owned by this worker. Call it
90
+ * once in the worker entry, after '@atolljs/node/shim':
91
+ *
92
+ * import '@atolljs/node/shim';
93
+ * import { serveHttp } from '@atolljs/node/http';
94
+ * import express from 'express';
95
+ *
96
+ * const app = express(); // any (req, res) handler works —
97
+ * app.get('/api', (_q, r) => …); // express/koa .callback()/fastify.server
98
+ * serveHttp(app); // sockets transferred by createHttpCluster
99
+ * serveHttp(app, { listen: 0 }); // internal listener for routeHttpGateway
100
+ *
101
+ * Accepts a RequestListener (wrapped in a new http.Server), an existing
102
+ * http.Server (e.g. `fastify().server` after ready()), or
103
+ * `{ handler }` / `{ server }`. A no-op on the main thread.
104
+ */
105
+ export declare function serveHttp(target: RequestListener | HttpServer | {
106
+ handler?: RequestListener;
107
+ server?: HttpServer;
108
+ }, options?: ServeHttpOptions): HttpServer;
109
+ export interface WorkerHttpPorts {
110
+ /** Announced internal listener ports, keyed by worker instance (live map). */
111
+ readonly ports: ReadonlyMap<Worker, number>;
112
+ /**
113
+ * Attach to any new workers in `source.workers` and re-query ones that
114
+ * haven't announced — call it before resolving routes so respawns (and
115
+ * announcements that raced the initial attach) are picked up.
116
+ */
117
+ refresh(): void;
118
+ /** Detach all message listeners. */
119
+ dispose(): void;
120
+ }
121
+ /**
122
+ * Collect HTTP_PORT announcements from a pool's workers — for the
123
+ * gateway/proxy topology where workers listen on internal TCP ports.
124
+ * Announcements are a handshake, not a broadcast: workers that haven't
125
+ * announced get an HTTP_PORT_QUERY so trackers attached late still learn
126
+ * their ports.
127
+ */
128
+ export declare function workerHttpPorts(source: WorkerSource): WorkerHttpPorts;
129
+ export interface ProxyToWorkerOptions {
130
+ /** The pool whose workers announce internal HTTP ports. */
131
+ pool: WorkerSource;
132
+ /** Announcement tracker — from {@link workerHttpPorts} (refreshed per request). */
133
+ tracker: WorkerHttpPorts;
134
+ /** Which worker serves this route — a slot index into the live
135
+ * `pool.workers` snapshot, or a selector (e.g. round-robin). */
136
+ worker: number | ((workers: readonly Worker[]) => Worker | undefined);
137
+ /**
138
+ * Prefix prepended to `req.url` before forwarding. When mounted via
139
+ * `app.use('/prefix', mw)` Express has already stripped the mount — set
140
+ * `to: '/prefix'` to restore it for the worker's routes.
141
+ */
142
+ to?: string;
143
+ }
144
+ /**
145
+ * A mountable RequestListener that proxies to one pool worker's internal
146
+ * listener — the piece that lets a host framework (Nest, Express, …) house
147
+ * part of its API inside workers without giving up its own server:
148
+ *
149
+ * const tracker = workerHttpPorts(pool);
150
+ * app.use('/api/housed', proxyToWorker({
151
+ * pool, tracker, to: '/api/housed',
152
+ * worker: (w) => w[i++ % w.length], // or 0 to pin a slot
153
+ * }));
154
+ */
155
+ export declare function proxyToWorker(options: ProxyToWorkerOptions): RequestListener;
156
+ /**
157
+ * The upgrade-side sibling of {@link proxyToWorker}: middleware never sees
158
+ * WebSocket handshakes (Node emits 'upgrade' on the server, bypassing
159
+ * request listeners), so attach the returned listener to your server:
160
+ *
161
+ * const tracker = workerHttpPorts(pool);
162
+ * app.use('/api/housed', proxyToWorker({ pool, tracker, to: '/api/housed', worker }));
163
+ * app.getHttpServer().on('upgrade',
164
+ * proxyUpgradeToWorker({ pool, tracker, worker })); // full URL — `to` not needed
165
+ *
166
+ * Unlike the middleware, an 'upgrade' listener receives the ORIGINAL url
167
+ * (nothing stripped it), so `to` is a rewrite prefix rather than a mount
168
+ * restore — usually omit it.
169
+ */
170
+ export declare function proxyUpgradeToWorker(options: ProxyToWorkerOptions): (req: IncomingMessage, socket: Socket, head: Buffer) => void;
171
+ export interface GatewayRoute {
172
+ /** URL prefix that owns this route, e.g. '/api/a/'. */
173
+ prefix: string;
174
+ /** Which pool worker serves the prefix — a slot index into the live
175
+ * `pool.workers` snapshot, or a selector. Respawns are picked up. */
176
+ worker: number | ((workers: readonly Worker[]) => Worker | undefined);
177
+ /** Rewrite target prefix — the worker sees `to + url.slice(prefix.length)`.
178
+ * Defaults to '/', so `/api/a/x` arrives at the worker as `/x`. */
179
+ to?: string;
180
+ }
181
+ export interface RouteHttpGatewayOptions {
182
+ /** The pool whose workers announce internal HTTP ports. */
183
+ pool: WorkerSource;
184
+ port?: number;
185
+ host?: string;
186
+ /** BYO http.Server (the gateway attaches its request handler). */
187
+ server?: HttpServer;
188
+ routes?: GatewayRoute[];
189
+ /** Everything unmatched runs on the main thread; defaults to 404. */
190
+ handler?: RequestListener;
191
+ /**
192
+ * Fallback for unmatched Upgrade (WebSocket) handshakes — e.g. attach your
193
+ * own ws server's handler to keep WS routes on the main thread. When
194
+ * omitted the socket is destroyed, unless another 'upgrade' listener is
195
+ * registered on the server (then it's left to that listener).
196
+ */
197
+ onUpgrade?: (req: IncomingMessage, socket: Socket, head: Buffer) => void;
198
+ onListen?: (server: HttpServer) => void;
199
+ }
200
+ export interface HttpGateway {
201
+ server: HttpServer;
202
+ /** Announced internal listener ports, keyed by worker instance (live map). */
203
+ ports: ReadonlyMap<Worker, number>;
204
+ close(): Promise<void>;
205
+ }
206
+ /** Main thread: parse HTTP once and proxy matched prefixes to worker-owned
207
+ * listeners — the mixed topology: some routes in worker A, some in worker
208
+ * B, the rest served right here. Upgrade (WebSocket) handshakes match the
209
+ * same prefix table and are TUNNELED — the handshake is replayed to the
210
+ * worker's listener and the two sockets spliced, so frames flow end-to-end
211
+ * without the main thread touching them again. */
212
+ export declare function routeHttpGateway(options: RouteHttpGatewayOptions): HttpGateway;
package/dist/http.js ADDED
@@ -0,0 +1,261 @@
1
+ // src/http.ts
2
+ import {
3
+ createServer as createHttpServer,
4
+ request as httpRequest,
5
+ Server as HttpServer
6
+ } from "node:http";
7
+ import {
8
+ connect as netConnect,
9
+ createServer as createNetServer
10
+ } from "node:net";
11
+ import { isMainThread, parentPort } from "node:worker_threads";
12
+ var HTTP_CONNECTION = "HTTP_CONNECTION";
13
+ var HTTP_PORT = "HTTP_PORT";
14
+ var HTTP_PORT_QUERY = "HTTP_PORT_QUERY";
15
+ var NODE_MAJOR = Number(process.versions.node.split(".")[0]);
16
+ var SOCKET_TRANSFER_SUPPORTED = NODE_MAJOR >= 26;
17
+ function assertSocketTransferSupported() {
18
+ if (!SOCKET_TRANSFER_SUPPORTED) {
19
+ throw new Error(
20
+ `serveHttp (without { listen }) requires Node.js >= 26 for net.Socket transfer across worker_threads (found ${process.versions.node}).`
21
+ );
22
+ }
23
+ }
24
+ function createHttpCluster(options) {
25
+ if (!SOCKET_TRANSFER_SUPPORTED) {
26
+ console.warn(
27
+ `[atoll] createHttpCluster skipped \u2014 net.Socket transfer requires Node.js >= 26 (found ${process.versions.node})`
28
+ );
29
+ return null;
30
+ }
31
+ let cursor = 0;
32
+ const route = options.route ?? ((_, workers) => workers.length === 0 ? void 0 : workers[cursor++ % workers.length]);
33
+ const server = options.server ?? createNetServer({ pauseOnConnect: true });
34
+ server.on("connection", (socket) => {
35
+ const worker = route(socket, options.pool.workers);
36
+ if (!worker) {
37
+ socket.destroy();
38
+ return;
39
+ }
40
+ try {
41
+ worker.postMessage({ type: HTTP_CONNECTION, socket }, [
42
+ socket
43
+ ]);
44
+ } catch (err) {
45
+ socket.destroy();
46
+ server.emit("error", err);
47
+ }
48
+ });
49
+ if (!options.server) {
50
+ server.listen(options.port ?? 8080, options.host, () => options.onListen?.(server));
51
+ } else {
52
+ options.onListen?.(server);
53
+ }
54
+ return {
55
+ server,
56
+ close: () => new Promise(
57
+ (resolve, reject) => server.close((err) => err ? reject(err) : resolve())
58
+ )
59
+ };
60
+ }
61
+ var fnv1a = (s) => {
62
+ let h = 2166136261;
63
+ for (let i = 0; i < s.length; i++) {
64
+ h ^= s.charCodeAt(i);
65
+ h = Math.imul(h, 16777619);
66
+ }
67
+ return h >>> 0;
68
+ };
69
+ function stickyByAddress(key = (s) => s.remoteAddress ?? "") {
70
+ return (socket, workers) => {
71
+ if (workers.length === 0) return void 0;
72
+ const k = key(socket);
73
+ let best;
74
+ let bestScore = -1;
75
+ workers.forEach((w, i) => {
76
+ const score = fnv1a(`${k}#${i}`);
77
+ if (score > bestScore) {
78
+ bestScore = score;
79
+ best = w;
80
+ }
81
+ });
82
+ return best;
83
+ };
84
+ }
85
+ function serveHttp(target, options) {
86
+ const server = target instanceof HttpServer ? target : typeof target === "function" ? createHttpServer(target) : target.server ?? createHttpServer(target.handler);
87
+ if (isMainThread || !parentPort) return server;
88
+ if (options?.listen === void 0) assertSocketTransferSupported();
89
+ let announcedPort;
90
+ parentPort.on("message", (msg) => {
91
+ if (msg?.type === HTTP_CONNECTION && msg.socket) {
92
+ server.emit("connection", msg.socket);
93
+ msg.socket.resume();
94
+ } else if (msg?.type === HTTP_PORT_QUERY && announcedPort !== void 0) {
95
+ parentPort.postMessage({ type: HTTP_PORT, port: announcedPort });
96
+ }
97
+ });
98
+ if (options?.listen !== void 0) {
99
+ server.listen(options.listen, options.host ?? "127.0.0.1", () => {
100
+ const addr = server.address();
101
+ announcedPort = typeof addr === "object" && addr ? addr.port : 0;
102
+ parentPort.postMessage({ type: HTTP_PORT, port: announcedPort });
103
+ });
104
+ }
105
+ return server;
106
+ }
107
+ function workerHttpPorts(source) {
108
+ const ports = /* @__PURE__ */ new Map();
109
+ const listeners = /* @__PURE__ */ new Map();
110
+ const refresh = () => {
111
+ for (const w of source.workers) {
112
+ if (!listeners.has(w)) {
113
+ const listener = (e) => {
114
+ const d = e.data;
115
+ if (d?.type === HTTP_PORT && typeof d.port === "number") ports.set(w, d.port);
116
+ };
117
+ listeners.set(w, listener);
118
+ w.addEventListener("message", listener);
119
+ }
120
+ if (!ports.has(w)) {
121
+ try {
122
+ w.postMessage({ type: HTTP_PORT_QUERY });
123
+ } catch {
124
+ }
125
+ }
126
+ }
127
+ };
128
+ refresh();
129
+ return {
130
+ ports,
131
+ refresh,
132
+ dispose: () => {
133
+ for (const [w, l] of listeners) w.removeEventListener("message", l);
134
+ listeners.clear();
135
+ }
136
+ };
137
+ }
138
+ var forwardToPort = (req, res, port, path) => {
139
+ const upstream = httpRequest(
140
+ {
141
+ host: "127.0.0.1",
142
+ port,
143
+ method: req.method,
144
+ path: path.startsWith("/") ? path : `/${path}`,
145
+ headers: { ...req.headers, host: `127.0.0.1:${port}` }
146
+ },
147
+ (ures) => {
148
+ res.writeHead(ures.statusCode ?? 502, ures.headers);
149
+ ures.pipe(res);
150
+ }
151
+ );
152
+ upstream.on("error", () => {
153
+ if (!res.headersSent) res.writeHead(502);
154
+ res.end();
155
+ });
156
+ req.pipe(upstream);
157
+ };
158
+ function proxyToWorker(options) {
159
+ return (req, res) => {
160
+ const port = resolveWorkerPort(options);
161
+ if (port === void 0) {
162
+ res.writeHead(503, { "content-type": "application/json" });
163
+ res.end(JSON.stringify({ error: "worker not listening yet" }));
164
+ return;
165
+ }
166
+ forwardToPort(req, res, port, (options.to ?? "") + (req.url ?? "/"));
167
+ };
168
+ }
169
+ var resolveWorkerPort = (options) => {
170
+ options.tracker.refresh();
171
+ const workers = options.pool.workers;
172
+ const worker = typeof options.worker === "number" ? workers[options.worker] : options.worker(workers);
173
+ return worker ? options.tracker.ports.get(worker) : void 0;
174
+ };
175
+ var tunnelUpgradeToPort = (req, socket, head, port, path) => {
176
+ const upstream = netConnect({ host: "127.0.0.1", port }, () => {
177
+ const lines = [`${req.method} ${path} HTTP/${req.httpVersion}`];
178
+ for (let i = 0; i < req.rawHeaders.length; i += 2) {
179
+ const key = req.rawHeaders[i];
180
+ const value = key.toLowerCase() === "host" ? `127.0.0.1:${port}` : req.rawHeaders[i + 1];
181
+ lines.push(`${key}: ${value}`);
182
+ }
183
+ upstream.write(lines.join("\r\n") + "\r\n\r\n");
184
+ if (head?.length) upstream.write(head);
185
+ socket.pipe(upstream).pipe(socket);
186
+ });
187
+ upstream.on("error", () => socket.destroy());
188
+ socket.on("error", () => upstream.destroy());
189
+ };
190
+ function proxyUpgradeToWorker(options) {
191
+ return (req, socket, head) => {
192
+ const port = resolveWorkerPort(options);
193
+ if (port === void 0) {
194
+ socket.destroy();
195
+ return;
196
+ }
197
+ tunnelUpgradeToPort(req, socket, head, port, (options.to ?? "") + (req.url ?? "/"));
198
+ };
199
+ }
200
+ function routeHttpGateway(options) {
201
+ const tracker = workerHttpPorts(options.pool);
202
+ const fallback = options.handler ?? ((_req, res) => res.writeHead(404).end());
203
+ const server = options.server ?? createHttpServer();
204
+ const resolveRoute = (url) => {
205
+ tracker.refresh();
206
+ const workers = options.pool.workers;
207
+ const route = options.routes?.find((r) => url.startsWith(r.prefix));
208
+ const worker = route ? typeof route.worker === "number" ? workers[route.worker] : route.worker(workers) : void 0;
209
+ return { route, port: worker ? tracker.ports.get(worker) : void 0 };
210
+ };
211
+ server.on("request", (req, res) => {
212
+ const url = req.url ?? "/";
213
+ const { route, port } = resolveRoute(url);
214
+ if (!route) {
215
+ fallback(req, res);
216
+ return;
217
+ }
218
+ if (port === void 0) {
219
+ res.writeHead(503, { "content-type": "application/json" });
220
+ res.end(JSON.stringify({ error: "worker not listening yet" }));
221
+ return;
222
+ }
223
+ forwardToPort(req, res, port, (route.to ?? "/") + url.slice(route.prefix.length));
224
+ });
225
+ server.on("upgrade", (req, socket, head) => {
226
+ const url = req.url ?? "/";
227
+ const { route, port } = resolveRoute(url);
228
+ if (!route || port === void 0) {
229
+ if (options.onUpgrade) {
230
+ options.onUpgrade(req, socket, head);
231
+ } else if (server.listenerCount("upgrade") <= 1) {
232
+ socket.destroy();
233
+ }
234
+ return;
235
+ }
236
+ tunnelUpgradeToPort(req, socket, head, port, (route.to ?? "/") + url.slice(route.prefix.length));
237
+ });
238
+ if (!options.server) {
239
+ server.listen(options.port ?? 8080, options.host, () => options.onListen?.(server));
240
+ } else {
241
+ options.onListen?.(server);
242
+ }
243
+ return {
244
+ server,
245
+ ports: tracker.ports,
246
+ close: () => new Promise(
247
+ (resolve, reject) => server.close((err) => err ? reject(err) : resolve())
248
+ )
249
+ };
250
+ }
251
+ export {
252
+ HTTP_PORT,
253
+ SOCKET_TRANSFER_SUPPORTED,
254
+ createHttpCluster,
255
+ proxyToWorker,
256
+ proxyUpgradeToWorker,
257
+ routeHttpGateway,
258
+ serveHttp,
259
+ stickyByAddress,
260
+ workerHttpPorts
261
+ };
@@ -0,0 +1,5 @@
1
+ export { NodeWorkerAdapter, createNodeWorker } from './worker.js';
2
+ export type { WorkerErrorEvent } from './worker.js';
3
+ export { createNodePool } from './pool.js';
4
+ export type { NodePoolConfig } from './pool.js';
5
+ export { bindSharedBuffer, withSharedBuffer } from './sharedBuffer.js';
package/dist/index.js ADDED
@@ -0,0 +1,127 @@
1
+ import {
2
+ WorkerPool,
3
+ bindSharedMemories
4
+ } from "./chunk-EEME4TDI.js";
5
+
6
+ // src/worker.ts
7
+ import { Worker as NodeWorker } from "node:worker_threads";
8
+ var NodeWorkerAdapter = class {
9
+ constructor(worker) {
10
+ this.worker = worker;
11
+ }
12
+ worker;
13
+ listeners = /* @__PURE__ */ new Map();
14
+ postMessage(message, transferList) {
15
+ if (transferList === void 0) {
16
+ this.worker.postMessage(message);
17
+ } else {
18
+ this.worker.postMessage(message, transferList);
19
+ }
20
+ }
21
+ terminate() {
22
+ void this.worker.terminate();
23
+ }
24
+ addEventListener(type, handler) {
25
+ let map = this.listeners.get(handler);
26
+ if (!map) this.listeners.set(handler, map = /* @__PURE__ */ new Map());
27
+ if (type === "message") {
28
+ const wrapped = (data) => handler({ data });
29
+ map.set("message", wrapped);
30
+ this.worker.on("message", wrapped);
31
+ } else {
32
+ const onError = (error) => handler({ type: "error", message: error.message, error });
33
+ const onExit = (code) => {
34
+ if (code !== 0) {
35
+ handler({ type: "error", message: `worker exited with code ${code}` });
36
+ }
37
+ };
38
+ map.set("error", onError);
39
+ map.set("exit", onExit);
40
+ this.worker.on("error", onError);
41
+ this.worker.on("exit", onExit);
42
+ }
43
+ }
44
+ removeEventListener(type, handler) {
45
+ const map = this.listeners.get(handler);
46
+ if (!map) return;
47
+ const events = type === "message" ? ["message"] : ["error", "exit"];
48
+ for (const ev of events) {
49
+ const wrapped = map.get(ev);
50
+ if (wrapped) {
51
+ this.worker.off(ev, wrapped);
52
+ map.delete(ev);
53
+ }
54
+ }
55
+ if (map.size === 0) this.listeners.delete(handler);
56
+ }
57
+ };
58
+ function createNodeWorker(source, options) {
59
+ const worker = source instanceof NodeWorker ? source : new NodeWorker(source, options);
60
+ return new NodeWorkerAdapter(worker);
61
+ }
62
+
63
+ // src/pool.ts
64
+ import { Worker as NodeWorker2 } from "node:worker_threads";
65
+ var toWorker = (w) => w instanceof NodeWorker2 ? createNodeWorker(w) : w;
66
+ function createNodePool(config) {
67
+ const { workerFile, worker, createWorker, ...rest } = config;
68
+ const file = workerFile ?? (typeof worker === "function" ? void 0 : worker);
69
+ const factory = createWorker ?? (typeof worker === "function" ? () => toWorker(worker()) : void 0);
70
+ return new WorkerPool({
71
+ ...rest,
72
+ createWorker: factory ?? (() => {
73
+ if (!file) {
74
+ throw new Error("createNodePool requires worker, workerFile or createWorker.");
75
+ }
76
+ return createNodeWorker(file);
77
+ })
78
+ });
79
+ }
80
+
81
+ // src/sharedBuffer.ts
82
+ import { isMainThread, parentPort, workerData } from "node:worker_threads";
83
+ var SHARED_BUFFER = "SHARED_BUFFER";
84
+ function withSharedBuffer(spawn, buffer) {
85
+ return () => {
86
+ const worker = spawn();
87
+ const shared = typeof buffer === "function" ? buffer() : buffer;
88
+ worker.postMessage({ type: SHARED_BUFFER, buffer: shared });
89
+ return worker;
90
+ };
91
+ }
92
+ function bindSharedBuffer(timeoutMs = 1e4) {
93
+ const fromWorkerData = workerData?.buffer;
94
+ if (fromWorkerData) {
95
+ bindSharedMemories(fromWorkerData);
96
+ return Promise.resolve(fromWorkerData);
97
+ }
98
+ if (isMainThread || !parentPort) {
99
+ return Promise.reject(new Error("bindSharedBuffer() must be called inside a worker"));
100
+ }
101
+ const port = parentPort;
102
+ return new Promise((resolve, reject) => {
103
+ const onMessage = (msg) => {
104
+ if (msg?.type !== SHARED_BUFFER) return;
105
+ clearTimeout(timer);
106
+ port.off("message", onMessage);
107
+ if (!msg.buffer) {
108
+ reject(new Error("bindSharedBuffer(): the producing side sent an empty buffer"));
109
+ return;
110
+ }
111
+ bindSharedMemories(msg.buffer);
112
+ resolve(msg.buffer);
113
+ };
114
+ const timer = setTimeout(() => {
115
+ port.off("message", onMessage);
116
+ reject(new Error(`bindSharedBuffer(): no shared buffer arrived within ${timeoutMs}ms`));
117
+ }, timeoutMs);
118
+ port.on("message", onMessage);
119
+ });
120
+ }
121
+ export {
122
+ NodeWorkerAdapter,
123
+ bindSharedBuffer,
124
+ createNodePool,
125
+ createNodeWorker,
126
+ withSharedBuffer
127
+ };
package/dist/pool.d.ts ADDED
@@ -0,0 +1,32 @@
1
+ import { Worker as NodeWorker } from 'node:worker_threads';
2
+ import { WorkerPool, type SharedSpec, type TaskMap, type WorkerPoolConfig } from '@atolljs/core';
3
+ /**
4
+ * WorkerPoolConfig for Node: workerUrl is replaced by a worker spec — a
5
+ * bundled file path, a `new Worker(new URL('./x.worker.ts', import.meta.url))`
6
+ * factory (webpack/esbuild detect the literal and emit the worker chunk), or
7
+ * an explicit createWorker for full control.
8
+ */
9
+ export interface NodePoolConfig<S extends SharedSpec = SharedSpec, T extends TaskMap = TaskMap> extends Omit<WorkerPoolConfig<S, T>, 'workerUrl' | 'createWorker'> {
10
+ /** Bundled worker entry (e.g. dist/incidents.worker.js). */
11
+ workerFile?: string;
12
+ /**
13
+ * Worker spec — the preferred form. A path/URL behaves like workerFile; a
14
+ * factory may return a node:worker_threads Worker (auto-adapted) or a
15
+ * DOM-style Worker (used as-is):
16
+ *
17
+ * worker: () => new Worker(new URL('./x.worker.ts', import.meta.url))
18
+ */
19
+ worker?: string | URL | (() => Worker | NodeWorker);
20
+ /** Worker factory; defaults to the node:worker_threads adapter. */
21
+ createWorker?: () => Worker;
22
+ }
23
+ /**
24
+ * Creates a WorkerPool backed by node:worker_threads. Identical to
25
+ * `new WorkerPool` except workers default to `new NodeWorker(workerFile)`
26
+ * wrapped in the EventTarget adapter — usable from any Node program, no
27
+ * framework required:
28
+ *
29
+ * const pool = createNodePool({ workerFile, sharedMemory, tasks });
30
+ * const pool = createNodePool({ worker: () => new Worker(new URL('./w.worker.ts', import.meta.url)), ... });
31
+ */
32
+ export declare function createNodePool<S extends SharedSpec, T extends TaskMap>(config: NodePoolConfig<S, T>): WorkerPool<S, T>;