|
1 | | -function patch($worker: typeof import("node:worker_threads"), $os: typeof import("node:os")) { |
2 | | - // This is technically not a part of the Worker polyfill, |
3 | | - // but Workers are used for multi-threading, so this is often |
4 | | - // needed when writing Worker code. |
5 | | - if (globalThis.navigator == null) { |
6 | | - globalThis.navigator = { |
7 | | - hardwareConcurrency: $os.cpus().length, |
8 | | - } as Navigator; |
9 | | - } |
10 | | - |
11 | | - globalThis.Worker = class Worker extends EventTarget { |
12 | | - private _worker: import("node:worker_threads").Worker; |
| 1 | +import * as $worker from "node:worker_threads"; |
| 2 | +import * as $os from "node:os"; |
| 3 | + |
| 4 | +// This is technically not a part of the Worker polyfill, |
| 5 | +// but Workers are used for multi-threading, so this is often |
| 6 | +// needed when writing Worker code. |
| 7 | +if (globalThis.navigator == null) { |
| 8 | + globalThis.navigator = { |
| 9 | + hardwareConcurrency: $os.cpus().length, |
| 10 | + } as Navigator; |
| 11 | +} |
13 | 12 |
|
14 | | - constructor(url: string | URL, options?: WorkerOptions | undefined) { |
15 | | - super(); |
| 13 | +globalThis.Worker = class Worker extends EventTarget { |
| 14 | + private _worker: import("node:worker_threads").Worker; |
16 | 15 |
|
17 | | - if (url instanceof URL) { |
18 | | - if (url.protocol !== "file:") { |
19 | | - throw new Error("Worker only supports file: URLs"); |
20 | | - } |
| 16 | + constructor(url: string | URL, options?: WorkerOptions | undefined) { |
| 17 | + super(); |
21 | 18 |
|
22 | | - url = url.href; |
23 | | - |
24 | | - } else { |
25 | | - throw new Error("Filepaths are unreliable, use `new URL(\"...\", import.meta.url)` instead."); |
| 19 | + if (url instanceof URL) { |
| 20 | + if (url.protocol !== "file:") { |
| 21 | + throw new Error("Worker only supports file: URLs"); |
26 | 22 | } |
27 | 23 |
|
28 | | - if (!options || options.type !== "module") { |
29 | | - throw new Error("Workers must use \`type: \"module\"\`"); |
30 | | - } |
| 24 | + url = url.href; |
31 | 25 |
|
32 | | - // This uses some funky stuff like `patch.toString()`. |
33 | | - // |
34 | | - // This is needed so that it can synchronously run the polyfill code |
35 | | - // inside of the worker. |
36 | | - // |
37 | | - // It can't use `require` because the file doesn't have a `.cjs` file extension. |
38 | | - // |
39 | | - // It can't use `import` because that's asynchronous, and the file path |
40 | | - // might be different if using a bundler. |
41 | | - const code = ` |
42 | | - ${patch.toString()} |
43 | | -
|
44 | | - // Inject the polyfill into the worker |
45 | | - patch(require("node:worker_threads"), require("node:os")); |
46 | | -
|
47 | | - const { workerData } = require("node:worker_threads"); |
48 | | -
|
49 | | - // This actually loads and runs the worker file |
50 | | - import(workerData.url) |
51 | | - .catch((e) => { |
52 | | - // TODO maybe it should send a message to the parent? |
53 | | - console.error(e.stack); |
54 | | - }); |
55 | | - `; |
56 | | - |
57 | | - this._worker = new $worker.Worker(code, { |
58 | | - eval: true, |
59 | | - workerData: { |
60 | | - url, |
61 | | - }, |
62 | | - }); |
63 | | - |
64 | | - this._worker.on("message", (data) => { |
65 | | - this.dispatchEvent(new MessageEvent("message", { data })); |
66 | | - }); |
67 | | - |
68 | | - this._worker.on("messageerror", (error) => { |
69 | | - throw new Error("UNIMPLEMENTED"); |
70 | | - }); |
71 | | - |
72 | | - this._worker.on("error", (error) => { |
73 | | - // TODO attach the error to the event somehow |
74 | | - const event = new Event("error"); |
75 | | - this.dispatchEvent(event); |
76 | | - }); |
| 26 | + } else { |
| 27 | + throw new Error("Filepaths are unreliable, use `new URL(\"...\", import.meta.url)` instead."); |
77 | 28 | } |
78 | 29 |
|
79 | | - set onmessage(f: () => void) { |
80 | | - throw new Error("UNIMPLEMENTED"); |
| 30 | + if (!options || options.type !== "module") { |
| 31 | + throw new Error("Workers must use \`type: \"module\"\`"); |
81 | 32 | } |
82 | 33 |
|
83 | | - set onmessageerror(f: () => void) { |
84 | | - throw new Error("UNIMPLEMENTED"); |
85 | | - } |
| 34 | + const code = ` |
| 35 | + const { workerData } = require("node:worker_threads"); |
| 36 | +
|
| 37 | + import(workerData.polyfill) |
| 38 | + .then(() => import(workerData.url)) |
| 39 | + .catch((e) => { |
| 40 | + // TODO maybe it should send a message to the parent? |
| 41 | + console.error(e.stack); |
| 42 | + }); |
| 43 | + `; |
| 44 | + |
| 45 | + this._worker = new $worker.Worker(code, { |
| 46 | + eval: true, |
| 47 | + workerData: { |
| 48 | + url, |
| 49 | + polyfill: new URL("node-polyfill.js", import.meta.url).href, |
| 50 | + }, |
| 51 | + }); |
86 | 52 |
|
87 | | - set onerror(f: () => void) { |
| 53 | + this._worker.on("message", (data) => { |
| 54 | + this.dispatchEvent(new MessageEvent("message", { data })); |
| 55 | + }); |
| 56 | + |
| 57 | + this._worker.on("messageerror", (error) => { |
88 | 58 | throw new Error("UNIMPLEMENTED"); |
89 | | - } |
| 59 | + }); |
90 | 60 |
|
91 | | - postMessage(message: any, transfer: Array<Transferable>): void; |
92 | | - postMessage(message: any, options?: StructuredSerializeOptions | undefined): void; |
93 | | - postMessage(value: any, transfer: any) { |
94 | | - this._worker.postMessage(value, transfer); |
95 | | - } |
| 61 | + this._worker.on("error", (error) => { |
| 62 | + // TODO attach the error to the event somehow |
| 63 | + const event = new Event("error"); |
| 64 | + this.dispatchEvent(event); |
| 65 | + }); |
| 66 | + } |
96 | 67 |
|
97 | | - terminate() { |
98 | | - this._worker.terminate(); |
99 | | - } |
| 68 | + set onmessage(f: () => void) { |
| 69 | + throw new Error("UNIMPLEMENTED"); |
| 70 | + } |
100 | 71 |
|
101 | | - // This is Node-specific, it allows the process to exit |
102 | | - // even if the Worker is still running. |
103 | | - unref() { |
104 | | - this._worker.unref(); |
105 | | - } |
106 | | - }; |
| 72 | + set onmessageerror(f: () => void) { |
| 73 | + throw new Error("UNIMPLEMENTED"); |
| 74 | + } |
107 | 75 |
|
| 76 | + set onerror(f: () => void) { |
| 77 | + throw new Error("UNIMPLEMENTED"); |
| 78 | + } |
108 | 79 |
|
109 | | - if (!$worker.isMainThread) { |
110 | | - const globals = globalThis as unknown as DedicatedWorkerGlobalScope; |
| 80 | + postMessage(message: any, transfer: Array<Transferable>): void; |
| 81 | + postMessage(message: any, options?: StructuredSerializeOptions | undefined): void; |
| 82 | + postMessage(value: any, transfer: any) { |
| 83 | + this._worker.postMessage(value, transfer); |
| 84 | + } |
111 | 85 |
|
112 | | - // This is used to create the onmessage, onmessageerror, and onerror setters |
113 | | - const makeSetter = (prop: string, event: string) => { |
114 | | - let oldvalue: () => void; |
| 86 | + terminate() { |
| 87 | + this._worker.terminate(); |
| 88 | + } |
115 | 89 |
|
116 | | - Object.defineProperty(globals, prop, { |
117 | | - get() { |
118 | | - return oldvalue; |
119 | | - }, |
120 | | - set(value) { |
121 | | - if (oldvalue) { |
122 | | - globals.removeEventListener(event, oldvalue); |
123 | | - } |
| 90 | + // This is Node-specific, it allows the process to exit |
| 91 | + // even if the Worker is still running. |
| 92 | + unref() { |
| 93 | + this._worker.unref(); |
| 94 | + } |
| 95 | +}; |
124 | 96 |
|
125 | | - oldvalue = value; |
126 | 97 |
|
127 | | - if (oldvalue) { |
128 | | - globals.addEventListener(event, oldvalue); |
129 | | - } |
130 | | - }, |
131 | | - }); |
132 | | - }; |
| 98 | +if (!$worker.isMainThread) { |
| 99 | + const globals = globalThis as unknown as DedicatedWorkerGlobalScope; |
133 | 100 |
|
134 | | - // This makes sure that `f` is only run once |
135 | | - const memoize = (f: () => void) => { |
136 | | - let run = false; |
| 101 | + // This is used to create the onmessage, onmessageerror, and onerror setters |
| 102 | + const makeSetter = (prop: string, event: string) => { |
| 103 | + let oldvalue: () => void; |
137 | 104 |
|
138 | | - return () => { |
139 | | - if (!run) { |
140 | | - run = true; |
141 | | - f(); |
| 105 | + Object.defineProperty(globals, prop, { |
| 106 | + get() { |
| 107 | + return oldvalue; |
| 108 | + }, |
| 109 | + set(value) { |
| 110 | + if (oldvalue) { |
| 111 | + globals.removeEventListener(event, oldvalue); |
142 | 112 | } |
143 | | - }; |
144 | | - }; |
145 | 113 |
|
| 114 | + oldvalue = value; |
146 | 115 |
|
147 | | - // We only start listening for messages / errors when the worker calls addEventListener |
148 | | - const startOnMessage = memoize(() => { |
149 | | - $worker.parentPort!.on("message", (data) => { |
150 | | - workerEvents.dispatchEvent(new MessageEvent("message", { data })); |
151 | | - }); |
| 116 | + if (oldvalue) { |
| 117 | + globals.addEventListener(event, oldvalue); |
| 118 | + } |
| 119 | + }, |
152 | 120 | }); |
| 121 | + }; |
153 | 122 |
|
154 | | - const startOnMessageError = memoize(() => { |
155 | | - throw new Error("UNIMPLEMENTED"); |
156 | | - }); |
| 123 | + // This makes sure that `f` is only run once |
| 124 | + const memoize = (f: () => void) => { |
| 125 | + let run = false; |
157 | 126 |
|
158 | | - const startOnError = memoize(() => { |
159 | | - $worker.parentPort!.on("error", (data) => { |
160 | | - workerEvents.dispatchEvent(new Event("error")); |
161 | | - }); |
| 127 | + return () => { |
| 128 | + if (!run) { |
| 129 | + run = true; |
| 130 | + f(); |
| 131 | + } |
| 132 | + }; |
| 133 | + }; |
| 134 | + |
| 135 | + |
| 136 | + // We only start listening for messages / errors when the worker calls addEventListener |
| 137 | + const startOnMessage = memoize(() => { |
| 138 | + $worker.parentPort!.on("message", (data) => { |
| 139 | + workerEvents.dispatchEvent(new MessageEvent("message", { data })); |
162 | 140 | }); |
| 141 | + }); |
163 | 142 |
|
| 143 | + const startOnMessageError = memoize(() => { |
| 144 | + throw new Error("UNIMPLEMENTED"); |
| 145 | + }); |
164 | 146 |
|
165 | | - // Node workers don't have top-level events, so we have to make our own |
166 | | - const workerEvents = new EventTarget(); |
| 147 | + const startOnError = memoize(() => { |
| 148 | + $worker.parentPort!.on("error", (data) => { |
| 149 | + workerEvents.dispatchEvent(new Event("error")); |
| 150 | + }); |
| 151 | + }); |
167 | 152 |
|
168 | | - globals.close = () => { |
169 | | - process.exit(); |
170 | | - }; |
171 | 153 |
|
172 | | - globals.addEventListener = (type: string, callback: EventListenerOrEventListenerObject | null, options?: boolean | EventListenerOptions | undefined) => { |
173 | | - workerEvents.addEventListener(type, callback, options); |
| 154 | + // Node workers don't have top-level events, so we have to make our own |
| 155 | + const workerEvents = new EventTarget(); |
174 | 156 |
|
175 | | - if (type === "message") { |
176 | | - startOnMessage(); |
177 | | - } else if (type === "messageerror") { |
178 | | - startOnMessageError(); |
179 | | - } else if (type === "error") { |
180 | | - startOnError(); |
181 | | - } |
182 | | - }; |
| 157 | + globals.close = () => { |
| 158 | + process.exit(); |
| 159 | + }; |
183 | 160 |
|
184 | | - globals.removeEventListener = (type: string, callback: EventListenerOrEventListenerObject | null, options?: boolean | EventListenerOptions | undefined) => { |
185 | | - workerEvents.removeEventListener(type, callback, options); |
186 | | - }; |
| 161 | + globals.addEventListener = (type: string, callback: EventListenerOrEventListenerObject | null, options?: boolean | EventListenerOptions | undefined) => { |
| 162 | + workerEvents.addEventListener(type, callback, options); |
187 | 163 |
|
188 | | - function postMessage(message: any, transfer: Transferable[]): void; |
189 | | - function postMessage(message: any, options?: StructuredSerializeOptions | undefined): void; |
190 | | - function postMessage(value: any, transfer: any) { |
191 | | - $worker.parentPort!.postMessage(value, transfer); |
| 164 | + if (type === "message") { |
| 165 | + startOnMessage(); |
| 166 | + } else if (type === "messageerror") { |
| 167 | + startOnMessageError(); |
| 168 | + } else if (type === "error") { |
| 169 | + startOnError(); |
192 | 170 | } |
| 171 | + }; |
193 | 172 |
|
194 | | - globals.postMessage = postMessage; |
| 173 | + globals.removeEventListener = (type: string, callback: EventListenerOrEventListenerObject | null, options?: boolean | EventListenerOptions | undefined) => { |
| 174 | + workerEvents.removeEventListener(type, callback, options); |
| 175 | + }; |
195 | 176 |
|
196 | | - makeSetter("onmessage", "message"); |
197 | | - makeSetter("onmessageerror", "messageerror"); |
198 | | - makeSetter("onerror", "error"); |
| 177 | + function postMessage(message: any, transfer: Transferable[]): void; |
| 178 | + function postMessage(message: any, options?: StructuredSerializeOptions | undefined): void; |
| 179 | + function postMessage(value: any, transfer: any) { |
| 180 | + $worker.parentPort!.postMessage(value, transfer); |
199 | 181 | } |
200 | | -} |
201 | | - |
202 | 182 |
|
203 | | -async function polyfill() { |
204 | | - const [$worker, $os] = await Promise.all([ |
205 | | - import("node:worker_threads"), |
206 | | - import("node:os"), |
207 | | - ]); |
| 183 | + globals.postMessage = postMessage; |
208 | 184 |
|
209 | | - patch($worker, $os); |
| 185 | + makeSetter("onmessage", "message"); |
| 186 | + makeSetter("onmessageerror", "messageerror"); |
| 187 | + makeSetter("onerror", "error"); |
210 | 188 | } |
211 | | - |
212 | | -if (globalThis.Worker == null) { |
213 | | - await polyfill(); |
214 | | -} |
215 | | - |
216 | | -export {}; |
0 commit comments