@atolljs/node 0.1.0
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/README.md +17 -0
- package/package.json +24 -0
- package/src/index.ts +6 -0
- package/src/pool.ts +64 -0
- package/src/shim.ts +11 -0
- package/src/worker.ts +95 -0
package/README.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# @atolljs/node
|
|
2
|
+
|
|
3
|
+
`node:worker_threads` runtime adapter for `@atolljs/core` — lets `WorkerPool`
|
|
4
|
+
and `connectWorker` run on Node's Worker (an EventEmitter) instead of the DOM
|
|
5
|
+
surface they expect.
|
|
6
|
+
|
|
7
|
+
- `createNodePool({ worker, sharedMemory?, tasks?, … })` — `WorkerPool` with the
|
|
8
|
+
adapter baked in. The `worker:` factory may return a `node:worker_threads.Worker`
|
|
9
|
+
directly, so `new Worker(new URL('./x.worker.ts', import.meta.url))` stays
|
|
10
|
+
bundler-detectable.
|
|
11
|
+
- `createNodeWorker(nodeWorker)` / `NodeWorkerAdapter` — wraps a Node Worker for
|
|
12
|
+
`connectWorker` or a hand-built `WorkerPool`.
|
|
13
|
+
- `@atolljs/node/shim` — worker-side entry shim; imports first and binds
|
|
14
|
+
`self = parentPort` before `defineWorker`'s bootstrap evaluates.
|
|
15
|
+
|
|
16
|
+
`SharedArrayBuffer` works in Node with no headers — cross-origin isolation is a
|
|
17
|
+
browser-only requirement.
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@atolljs/node",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Node runtime adapter — node:worker_threads pools, no framework",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./src/index.ts",
|
|
8
|
+
"./shim": "./src/shim.ts"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src"
|
|
12
|
+
],
|
|
13
|
+
"publishConfig": {
|
|
14
|
+
"access": "public"
|
|
15
|
+
},
|
|
16
|
+
"peerDependencies": {
|
|
17
|
+
"@atolljs/core": "0.1.0"
|
|
18
|
+
},
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "https://github.com/jwhenry3/atolljs",
|
|
22
|
+
"directory": "packages/node"
|
|
23
|
+
}
|
|
24
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Node runtime adapter for the Atoll — node:worker_threads pools without a
|
|
2
|
+
// framework. Worker entries must import '@atolljs/node/shim' first.
|
|
3
|
+
export { NodeWorkerAdapter, createNodeWorker } from './worker';
|
|
4
|
+
export type { WorkerErrorEvent } from './worker';
|
|
5
|
+
export { createNodePool } from './pool';
|
|
6
|
+
export type { NodePoolConfig } from './pool';
|
package/src/pool.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Worker as NodeWorker } from 'node:worker_threads';
|
|
2
|
+
import {
|
|
3
|
+
WorkerPool,
|
|
4
|
+
type SharedSpec,
|
|
5
|
+
type TaskMap,
|
|
6
|
+
type WorkerPoolConfig,
|
|
7
|
+
} from '@atolljs/core';
|
|
8
|
+
import { createNodeWorker } from './worker';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* WorkerPoolConfig for Node: workerUrl is replaced by a worker spec — a
|
|
12
|
+
* bundled file path, a `new Worker(new URL('./x.worker.ts', import.meta.url))`
|
|
13
|
+
* factory (webpack/esbuild detect the literal and emit the worker chunk), or
|
|
14
|
+
* an explicit createWorker for full control.
|
|
15
|
+
*/
|
|
16
|
+
export interface NodePoolConfig<S extends SharedSpec = SharedSpec, T extends TaskMap = TaskMap>
|
|
17
|
+
extends Omit<WorkerPoolConfig<S, T>, 'workerUrl' | 'createWorker'> {
|
|
18
|
+
/** Bundled worker entry (e.g. dist/incidents.worker.js). */
|
|
19
|
+
workerFile?: string;
|
|
20
|
+
/**
|
|
21
|
+
* Worker spec — the preferred form. A path/URL behaves like workerFile; a
|
|
22
|
+
* factory may return a node:worker_threads Worker (auto-adapted) or a
|
|
23
|
+
* DOM-style Worker (used as-is):
|
|
24
|
+
*
|
|
25
|
+
* worker: () => new Worker(new URL('./x.worker.ts', import.meta.url))
|
|
26
|
+
*/
|
|
27
|
+
worker?: string | URL | (() => Worker | NodeWorker);
|
|
28
|
+
/** Worker factory; defaults to the node:worker_threads adapter. */
|
|
29
|
+
createWorker?: () => Worker;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Adapts whatever the user spawned into the DOM Worker surface the pool needs. */
|
|
33
|
+
const toWorker = (w: Worker | NodeWorker): Worker =>
|
|
34
|
+
w instanceof NodeWorker ? createNodeWorker(w) : w;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Creates a WorkerPool backed by node:worker_threads. Identical to
|
|
38
|
+
* `new WorkerPool` except workers default to `new NodeWorker(workerFile)`
|
|
39
|
+
* wrapped in the EventTarget adapter — usable from any Node program, no
|
|
40
|
+
* framework required:
|
|
41
|
+
*
|
|
42
|
+
* const pool = createNodePool({ workerFile, sharedMemory, tasks });
|
|
43
|
+
* const pool = createNodePool({ worker: () => new Worker(new URL('./w.worker.ts', import.meta.url)), ... });
|
|
44
|
+
*/
|
|
45
|
+
export function createNodePool<S extends SharedSpec, T extends TaskMap>(
|
|
46
|
+
config: NodePoolConfig<S, T>,
|
|
47
|
+
): WorkerPool<S, T> {
|
|
48
|
+
const { workerFile, worker, createWorker, ...rest } = config;
|
|
49
|
+
const file = workerFile ?? (typeof worker === 'function' ? undefined : worker);
|
|
50
|
+
const factory =
|
|
51
|
+
createWorker ??
|
|
52
|
+
(typeof worker === 'function' ? () => toWorker(worker()) : undefined);
|
|
53
|
+
return new WorkerPool({
|
|
54
|
+
...rest,
|
|
55
|
+
createWorker:
|
|
56
|
+
factory ??
|
|
57
|
+
(() => {
|
|
58
|
+
if (!file) {
|
|
59
|
+
throw new Error('createNodePool requires worker, workerFile or createWorker.');
|
|
60
|
+
}
|
|
61
|
+
return createNodeWorker(file);
|
|
62
|
+
}),
|
|
63
|
+
});
|
|
64
|
+
}
|
package/src/shim.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Import this FIRST in a Node worker entry, before workerBootstrap.
|
|
2
|
+
// node:worker_threads has no `self` global — the SDK's worker bootstrap
|
|
3
|
+
// expects a DOM-style `self` carrying onmessage/postMessage, so we bind the
|
|
4
|
+
// worker's parent MessagePort as `self` before the bootstrap module
|
|
5
|
+
// evaluates. ESM/CJS import order guarantees this runs first.
|
|
6
|
+
import { isMainThread, parentPort } from 'node:worker_threads';
|
|
7
|
+
|
|
8
|
+
// Guarded so an accidental main-thread import is a no-op (parentPort is null).
|
|
9
|
+
if (!isMainThread && parentPort) {
|
|
10
|
+
(globalThis as { self?: unknown }).self = parentPort;
|
|
11
|
+
}
|
package/src/worker.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { Worker as NodeWorker } from 'node:worker_threads';
|
|
2
|
+
|
|
3
|
+
export interface WorkerErrorEvent {
|
|
4
|
+
type: 'error';
|
|
5
|
+
message: string;
|
|
6
|
+
error?: unknown;
|
|
7
|
+
}
|
|
8
|
+
type EventHandler = (event: any) => void;
|
|
9
|
+
type NodeEvent = 'message' | 'error' | 'exit';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Adapts node:worker_threads.Worker to the DOM Worker surface that
|
|
13
|
+
* WorkerPool expects (`postMessage`, `terminate`, add/removeEventListener
|
|
14
|
+
* with `event.data`). Node's Worker is an EventEmitter — 'message' delivers
|
|
15
|
+
* the value directly, so handlers get wrapped and tracked by identity so
|
|
16
|
+
* removeEventListener can detach the exact listener the pool installed.
|
|
17
|
+
*
|
|
18
|
+
* 'error' listeners additionally observe a Node 'exit' with code ≠ 0 —
|
|
19
|
+
* surfaced as `{ type: 'error', message }` so the pool's crash/respawn
|
|
20
|
+
* path sees silent exits too. `exit(0)` is a clean shutdown, not an error.
|
|
21
|
+
*/
|
|
22
|
+
export class NodeWorkerAdapter {
|
|
23
|
+
private readonly listeners = new Map<EventHandler, Map<NodeEvent, (...args: any[]) => void>>();
|
|
24
|
+
|
|
25
|
+
constructor(private readonly worker: NodeWorker) {}
|
|
26
|
+
|
|
27
|
+
postMessage(message: unknown): void {
|
|
28
|
+
this.worker.postMessage(message);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
terminate(): void {
|
|
32
|
+
void this.worker.terminate();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
addEventListener(type: 'message', handler: (event: { data: unknown }) => void): void;
|
|
36
|
+
addEventListener(type: 'error', handler: (event: WorkerErrorEvent) => void): void;
|
|
37
|
+
addEventListener(type: 'message' | 'error', handler: EventHandler): void {
|
|
38
|
+
let map = this.listeners.get(handler);
|
|
39
|
+
if (!map) this.listeners.set(handler, (map = new Map()));
|
|
40
|
+
if (type === 'message') {
|
|
41
|
+
const wrapped = (data: unknown) => handler({ data });
|
|
42
|
+
map.set('message', wrapped);
|
|
43
|
+
this.worker.on('message', wrapped);
|
|
44
|
+
} else {
|
|
45
|
+
const onError = (error: Error) =>
|
|
46
|
+
handler({ type: 'error', message: error.message, error });
|
|
47
|
+
const onExit = (code: number) => {
|
|
48
|
+
if (code !== 0) {
|
|
49
|
+
handler({ type: 'error', message: `worker exited with code ${code}` });
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
map.set('error', onError);
|
|
53
|
+
map.set('exit', onExit);
|
|
54
|
+
this.worker.on('error', onError);
|
|
55
|
+
this.worker.on('exit', onExit);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
removeEventListener(type: 'message', handler: (event: { data: unknown }) => void): void;
|
|
60
|
+
removeEventListener(type: 'error', handler: (event: WorkerErrorEvent) => void): void;
|
|
61
|
+
removeEventListener(type: 'message' | 'error', handler: EventHandler): void {
|
|
62
|
+
const map = this.listeners.get(handler);
|
|
63
|
+
if (!map) return;
|
|
64
|
+
const events: NodeEvent[] = type === 'message' ? ['message'] : ['error', 'exit'];
|
|
65
|
+
for (const ev of events) {
|
|
66
|
+
const wrapped = map.get(ev);
|
|
67
|
+
if (wrapped) {
|
|
68
|
+
this.worker.off(ev, wrapped);
|
|
69
|
+
map.delete(ev);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (map.size === 0) this.listeners.delete(handler);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Wraps node:worker_threads.Worker creation. Pass a file/URL to spawn, or an
|
|
78
|
+
* already-constructed Worker to adapt — the latter enables the
|
|
79
|
+
* bundler-detectable form in pool configs:
|
|
80
|
+
*
|
|
81
|
+
* createWorker: () => createNodeWorker(
|
|
82
|
+
* new NodeWorker(new URL('./my.worker.ts', import.meta.url))
|
|
83
|
+
* )
|
|
84
|
+
*
|
|
85
|
+
* webpack compiles the referenced entry into its own chunk and rewrites the
|
|
86
|
+
* URL to the emitted file — the config points at the TS source, never at a
|
|
87
|
+
* dist filename.
|
|
88
|
+
*/
|
|
89
|
+
export function createNodeWorker(
|
|
90
|
+
source: string | URL | NodeWorker,
|
|
91
|
+
options?: ConstructorParameters<typeof NodeWorker>[1],
|
|
92
|
+
): Worker {
|
|
93
|
+
const worker = source instanceof NodeWorker ? source : new NodeWorker(source, options);
|
|
94
|
+
return new NodeWorkerAdapter(worker) as unknown as Worker;
|
|
95
|
+
}
|