@b9g/node-webworker 0.1.2 → 0.2.0-beta.1

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 CHANGED
@@ -6,7 +6,7 @@ Minimal Web Worker shim for Node.js until native support arrives.
6
6
 
7
7
  Node.js lacks native Web Worker support, despite being a web standard since 2009. This package provides a minimal, reliable shim using Node.js `worker_threads` until native support is added.
8
8
 
9
- **🔗 Canonical Issue:** https://github.com/nodejs/node/issues/43583
9
+ **Canonical Issue:** https://github.com/nodejs/node/issues/43583
10
10
  **Please 👍 and comment** on the issue to show demand for native Web Worker support!
11
11
 
12
12
  ## Installation
@@ -18,7 +18,7 @@ npm install @b9g/node-webworker
18
18
  ## Usage
19
19
 
20
20
  ```typescript
21
- import { Worker } from '@b9g/node-webworker';
21
+ import {Worker} from '@b9g/node-webworker';
22
22
 
23
23
  // Create a worker (same API as Web Workers)
24
24
  const worker = new Worker('./worker.js', { type: 'module' });
@@ -32,22 +32,34 @@ worker.addEventListener('message', (event) => {
32
32
  worker.postMessage({ hello: 'world' });
33
33
 
34
34
  // Terminate when done
35
- await worker.terminate();
35
+ worker.terminate();
36
36
  ```
37
37
 
38
+ ## Exports
39
+
40
+ ### Classes
41
+
42
+ - `Worker` - Web Worker implementation using Node.js worker_threads
43
+ - `MessageEvent` - Event class for worker messages
44
+ - `ErrorEvent` - Event class for worker errors
45
+
46
+ ### Default Export
47
+
48
+ - `Worker` - The Worker class
49
+
38
50
  ## Features
39
51
 
40
- - **Standards-compliant API** - Drop-in replacement for Web Workers
41
- - **ES Module support** - Works with modern JavaScript
42
- - **Minimal overhead** - Thin wrapper around `worker_threads`
43
- - **Error handling** - Proper event forwarding
44
- - **Clean termination** - Resource cleanup
52
+ - **Standards-compliant API** - Drop-in replacement for Web Workers
53
+ - **ES Module support** - Works with modern JavaScript
54
+ - **Minimal overhead** - Thin wrapper around `worker_threads`
55
+ - **Error handling** - Proper event forwarding
56
+ - **Clean termination** - Resource cleanup
45
57
 
46
58
  ## Limitations
47
59
 
48
- - **Transferable objects** - Limited support (logs warning)
49
60
  - **Node.js only** - Don't use this in browsers (they have native Web Workers)
50
- - **Basic API** - Only core Worker features, not full spec
61
+ - **Module workers only** - Classic workers with `importScripts()` not supported
62
+ - **Serialization differences** - Uses Node.js structured clone, not web's algorithm
51
63
 
52
64
  ## Deprecation Notice
53
65
 
@@ -57,4 +69,4 @@ We maintain this as a temporary workaround. Please help push for native support
57
69
 
58
70
  ## License
59
71
 
60
- MIT
72
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@b9g/node-webworker",
3
- "version": "0.1.2",
3
+ "version": "0.2.0-beta.1",
4
4
  "description": "Minimal Web Worker shim for Node.js until native support arrives",
5
5
  "keywords": [
6
6
  "worker",
@@ -11,8 +11,7 @@
11
11
  ],
12
12
  "dependencies": {},
13
13
  "devDependencies": {
14
- "@b9g/libuild": "^0.1.11",
15
- "bun-types": "latest",
14
+ "@b9g/libuild": "^0.1.18",
16
15
  "@types/node": "^18.0.0"
17
16
  },
18
17
  "engines": {
@@ -34,14 +33,6 @@
34
33
  "./index.js": {
35
34
  "types": "./src/index.d.ts",
36
35
  "import": "./src/index.js"
37
- },
38
- "./worker-wrapper": {
39
- "types": "./src/worker-wrapper.d.ts",
40
- "import": "./src/worker-wrapper.js"
41
- },
42
- "./worker-wrapper.js": {
43
- "types": "./src/worker-wrapper.d.ts",
44
- "import": "./src/worker-wrapper.js"
45
36
  }
46
37
  }
47
38
  }
package/src/index.d.ts CHANGED
@@ -6,20 +6,27 @@
6
6
  *
7
7
  * @see https://github.com/nodejs/node/issues/43583
8
8
  */
9
- import { Worker as NodeWorker } from 'worker_threads';
10
9
  /**
11
- * Event-like object for message events
10
+ * Message event for worker communication
12
11
  */
13
- export interface MessageEvent {
12
+ export declare class MessageEvent extends Event {
14
13
  readonly data: any;
15
- readonly type: 'message';
14
+ constructor(data: any);
16
15
  }
17
16
  /**
18
- * Error event object
17
+ * Error event for worker errors
19
18
  */
20
- export interface ErrorEvent {
19
+ export declare class ErrorEvent extends Event {
21
20
  readonly error: Error;
22
- readonly type: 'error';
21
+ constructor(error: Error);
22
+ }
23
+ /**
24
+ * Close event for worker termination
25
+ * Includes exit code for crash detection
26
+ */
27
+ export declare class CloseEvent extends Event {
28
+ readonly code: number;
29
+ constructor(code: number);
23
30
  }
24
31
  /**
25
32
  * Web Worker API implementation using Node.js worker_threads
@@ -28,11 +35,14 @@ export interface ErrorEvent {
28
35
  * to Node.js worker_threads underneath.
29
36
  */
30
37
  export declare class Worker {
31
- private nodeWorker;
32
- private messageListeners;
33
- private errorListeners;
34
- constructor(scriptURL: string, options?: {
35
- type?: 'classic' | 'module';
38
+ #private;
39
+ onmessage: ((event: MessageEvent) => void) | null;
40
+ onerror: ((event: ErrorEvent) => void) | null;
41
+ onmessageerror: ((event: MessageEvent) => void) | null;
42
+ onclose: ((event: CloseEvent) => void) | null;
43
+ constructor(scriptURL: string | URL, _options?: {
44
+ type?: "classic" | "module";
45
+ env?: Record<string, string>;
36
46
  });
37
47
  /**
38
48
  * Send a message to the worker
@@ -41,20 +51,20 @@ export declare class Worker {
41
51
  /**
42
52
  * Add an event listener (Web Worker API)
43
53
  */
44
- addEventListener(type: 'message', listener: (event: MessageEvent) => void): void;
45
- addEventListener(type: 'error', listener: (event: ErrorEvent) => void): void;
54
+ addEventListener(type: "message", listener: (event: MessageEvent) => void): void;
55
+ addEventListener(type: "error", listener: (event: ErrorEvent) => void): void;
56
+ addEventListener(type: "messageerror", listener: (event: MessageEvent) => void): void;
57
+ addEventListener(type: "close", listener: (event: CloseEvent) => void): void;
46
58
  /**
47
59
  * Remove an event listener
48
60
  */
49
- removeEventListener(type: 'message', listener: (event: MessageEvent) => void): void;
50
- removeEventListener(type: 'error', listener: (event: ErrorEvent) => void): void;
51
- /**
52
- * Terminate the worker
53
- */
54
- terminate(): Promise<number>;
61
+ removeEventListener(type: "message", listener: (event: MessageEvent) => void): void;
62
+ removeEventListener(type: "error", listener: (event: ErrorEvent) => void): void;
63
+ removeEventListener(type: "messageerror", listener: (event: MessageEvent) => void): void;
64
+ removeEventListener(type: "close", listener: (event: CloseEvent) => void): void;
55
65
  /**
56
- * Get the underlying Node.js Worker (for advanced usage)
66
+ * Terminate the worker (Web Worker standard - returns void, not a promise)
57
67
  */
58
- get nodeWorker_(): NodeWorker;
68
+ terminate(): void;
59
69
  }
60
70
  export default Worker;
package/src/index.js CHANGED
@@ -1,87 +1,190 @@
1
1
  /// <reference types="./index.d.ts" />
2
2
  // src/index.ts
3
3
  import { Worker as NodeWorker } from "worker_threads";
4
- import { fileURLToPath } from "url";
5
- import { dirname, join } from "path";
4
+ var MessageEvent = class extends Event {
5
+ data;
6
+ constructor(data) {
7
+ super("message");
8
+ this.data = data;
9
+ }
10
+ };
11
+ var ErrorEvent = class extends Event {
12
+ error;
13
+ constructor(error) {
14
+ super("error");
15
+ this.error = error;
16
+ }
17
+ };
18
+ var CloseEvent = class extends Event {
19
+ code;
20
+ constructor(code) {
21
+ super("close");
22
+ this.code = code;
23
+ }
24
+ };
25
+ var WORKER_WRAPPER_CODE = `import{parentPort as p}from"worker_threads";const l=new Set();globalThis.onmessage=null;globalThis.onmessageerror=null;globalThis.postMessage=(d,t)=>t?.length?p.postMessage(d,t):p.postMessage(d);globalThis.self=globalThis;globalThis.addEventListener=(t,f)=>t==="message"&&l.add(f);globalThis.removeEventListener=(t,f)=>t==="message"&&l.delete(f);p.on("message",d=>{const e={data:d,type:"message"};globalThis.onmessage?.(e);l.forEach(f=>f(e))});const u=process.env.WORKER_SCRIPT_URL;if(u)await import(u);else throw Error("WORKER_SCRIPT_URL not set");`;
26
+ var WORKER_WRAPPER_DATA_URL = new URL(
27
+ `data:text/javascript,${encodeURIComponent(WORKER_WRAPPER_CODE)}`
28
+ );
6
29
  var Worker = class {
7
- nodeWorker;
8
- messageListeners = /* @__PURE__ */ new Set();
9
- errorListeners = /* @__PURE__ */ new Set();
10
- constructor(scriptURL, options) {
11
- const __filename = fileURLToPath(import.meta.url);
12
- const __dirname = dirname(__filename);
13
- const wrapperScript = join(__dirname, "worker-wrapper.js");
14
- this.nodeWorker = new NodeWorker(wrapperScript, {
15
- type: "module",
30
+ #nodeWorker;
31
+ #messageListeners;
32
+ #errorListeners;
33
+ #messageerrorListeners;
34
+ #closeListeners;
35
+ // Web Worker standard properties
36
+ onmessage;
37
+ onerror;
38
+ onmessageerror;
39
+ onclose;
40
+ constructor(scriptURL, _options) {
41
+ this.#messageListeners = /* @__PURE__ */ new Set();
42
+ this.#errorListeners = /* @__PURE__ */ new Set();
43
+ this.#messageerrorListeners = /* @__PURE__ */ new Set();
44
+ this.#closeListeners = /* @__PURE__ */ new Set();
45
+ this.onmessage = null;
46
+ this.onerror = null;
47
+ this.onmessageerror = null;
48
+ this.onclose = null;
49
+ const scriptURLString = scriptURL.toString();
50
+ let workerScriptURL = scriptURLString;
51
+ if (!scriptURLString.startsWith("file://") && !scriptURLString.startsWith("data:")) {
52
+ if (scriptURLString.startsWith("./") || scriptURLString.startsWith("../")) {
53
+ throw new Error(
54
+ "Relative paths are not supported. Use new Worker(new URL('./worker.js', import.meta.url)) instead."
55
+ );
56
+ }
57
+ workerScriptURL = `file://${scriptURLString}`;
58
+ }
59
+ this.#nodeWorker = new NodeWorker(WORKER_WRAPPER_DATA_URL, {
60
+ ...{ type: "module" },
16
61
  env: {
62
+ // eslint-disable-next-line no-restricted-properties -- Workers inherit parent env
17
63
  ...process.env,
18
- WORKER_SCRIPT_URL: scriptURL
64
+ ..._options?.env,
65
+ WORKER_SCRIPT_URL: workerScriptURL
19
66
  }
20
67
  });
21
- this.nodeWorker.on("message", (data) => {
22
- const event = { data, type: "message" };
23
- this.messageListeners.forEach((listener) => {
68
+ this.#setupEventForwarding();
69
+ }
70
+ /**
71
+ * Report a close event when the worker exits
72
+ */
73
+ #reportClose(code) {
74
+ const event = new CloseEvent(code);
75
+ if (this.onclose) {
76
+ this.onclose(event);
77
+ }
78
+ this.#closeListeners.forEach((listener) => {
79
+ listener(event);
80
+ });
81
+ }
82
+ /**
83
+ * Report an error through the error event mechanism
84
+ */
85
+ #reportError(error) {
86
+ const event = new ErrorEvent(error);
87
+ if (this.onerror) {
88
+ this.onerror(event);
89
+ }
90
+ this.#errorListeners.forEach((listener) => {
91
+ listener(event);
92
+ });
93
+ }
94
+ /**
95
+ * Set up event forwarding from Node.js Worker to Web Worker API
96
+ */
97
+ #setupEventForwarding() {
98
+ this.#nodeWorker.on("message", (data) => {
99
+ const event = new MessageEvent(data);
100
+ if (this.onmessage) {
24
101
  try {
25
- listener(event);
102
+ this.onmessage(event);
26
103
  } catch (error) {
27
- console.error("[node-webworker] Error in message listener:", error);
104
+ this.#reportError(error);
28
105
  }
29
- });
30
- });
31
- this.nodeWorker.on("error", (error) => {
32
- const event = { error, type: "error" };
33
- this.errorListeners.forEach((listener) => {
106
+ }
107
+ this.#messageListeners.forEach((listener) => {
34
108
  try {
35
109
  listener(event);
36
- } catch (listenerError) {
37
- console.error("[node-webworker] Error in error listener:", listenerError);
110
+ } catch (error) {
111
+ this.#reportError(error);
38
112
  }
39
113
  });
40
114
  });
115
+ this.#nodeWorker.on("error", (error) => {
116
+ this.#reportError(error);
117
+ });
118
+ this.#nodeWorker.on("messageerror", (data) => {
119
+ const event = new MessageEvent(data);
120
+ if (this.onmessageerror) {
121
+ this.onmessageerror(event);
122
+ }
123
+ this.#messageerrorListeners.forEach((listener) => {
124
+ listener(event);
125
+ });
126
+ });
127
+ this.#nodeWorker.on("exit", (code) => {
128
+ this.#reportClose(code);
129
+ });
41
130
  }
42
131
  /**
43
132
  * Send a message to the worker
44
133
  */
45
134
  postMessage(message, transfer) {
46
135
  if (transfer && transfer.length > 0) {
47
- console.warn("[node-webworker] Transferable objects not fully supported");
136
+ this.#nodeWorker.postMessage(message, transfer);
137
+ } else {
138
+ this.#nodeWorker.postMessage(message);
48
139
  }
49
- this.nodeWorker.postMessage(message);
50
140
  }
51
141
  addEventListener(type, listener) {
52
142
  if (type === "message") {
53
- this.messageListeners.add(listener);
143
+ this.#messageListeners.add(listener);
54
144
  } else if (type === "error") {
55
- this.errorListeners.add(listener);
56
- } else {
57
- console.warn(`[node-webworker] Unsupported event type: ${type}`);
145
+ this.#errorListeners.add(listener);
146
+ } else if (type === "messageerror") {
147
+ this.#messageerrorListeners.add(
148
+ listener
149
+ );
150
+ } else if (type === "close") {
151
+ this.#closeListeners.add(listener);
58
152
  }
59
153
  }
60
154
  removeEventListener(type, listener) {
61
155
  if (type === "message") {
62
- this.messageListeners.delete(listener);
156
+ this.#messageListeners.delete(listener);
63
157
  } else if (type === "error") {
64
- this.errorListeners.delete(listener);
158
+ this.#errorListeners.delete(listener);
159
+ } else if (type === "messageerror") {
160
+ this.#messageerrorListeners.delete(
161
+ listener
162
+ );
163
+ } else if (type === "close") {
164
+ this.#closeListeners.delete(listener);
65
165
  }
66
166
  }
67
167
  /**
68
- * Terminate the worker
168
+ * Terminate the worker (Web Worker standard - returns void, not a promise)
69
169
  */
70
- async terminate() {
71
- const exitCode = await this.nodeWorker.terminate();
72
- this.messageListeners.clear();
73
- this.errorListeners.clear();
74
- return exitCode;
75
- }
76
- /**
77
- * Get the underlying Node.js Worker (for advanced usage)
78
- */
79
- get nodeWorker_() {
80
- return this.nodeWorker;
170
+ terminate() {
171
+ this.#nodeWorker.terminate().catch(() => {
172
+ });
173
+ this.#messageListeners.clear();
174
+ this.#errorListeners.clear();
175
+ this.#messageerrorListeners.clear();
176
+ this.#closeListeners.clear();
177
+ this.onmessage = null;
178
+ this.onerror = null;
179
+ this.onmessageerror = null;
180
+ this.onclose = null;
81
181
  }
82
182
  };
83
183
  var src_default = Worker;
84
184
  export {
185
+ CloseEvent,
186
+ ErrorEvent,
187
+ MessageEvent,
85
188
  Worker,
86
189
  src_default as default
87
190
  };
@@ -1,15 +0,0 @@
1
- // src/worker-wrapper.js
2
- import { parentPort } from "worker_threads";
3
- globalThis.onmessage = null;
4
- globalThis.postMessage = (data) => parentPort.postMessage(data);
5
- parentPort.on("message", (data) => {
6
- if (globalThis.onmessage) {
7
- globalThis.onmessage({ data, type: "message" });
8
- }
9
- });
10
- var WORKER_SCRIPT_URL = process.env.WORKER_SCRIPT_URL;
11
- if (WORKER_SCRIPT_URL) {
12
- await import(WORKER_SCRIPT_URL);
13
- } else {
14
- throw new Error("WORKER_SCRIPT_URL environment variable not set");
15
- }