@nadrif/thread 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.
@@ -0,0 +1,347 @@
1
+ /**
2
+ * @file Cross-platform Worker factory for the thread library.
3
+ *
4
+ * Abstracts Worker creation across:
5
+ * - **Browser** — `new Worker(blobUrl)` with Blob URLs
6
+ * - **Node.js** — `worker_threads.Worker` with `eval` or `data:` URLs
7
+ * - **Bun** — `worker_threads.Worker` (Bun's built-in)
8
+ * - **Deno** — `new Worker(url)` (Deno's Worker API)
9
+ *
10
+ * The factory returns a **unified Worker interface** that exposes the
11
+ * same API (`postMessage`, `terminate`, `onmessage`, `onerror`) regardless
12
+ * of the underlying platform.
13
+ *
14
+ * @example
15
+ * ```js
16
+ * import { createWorker, terminateWorker, postToWorker } from './worker-factory.js';
17
+ *
18
+ * const worker = createWorker(scriptSource, { type: 'module' });
19
+ * worker.onmessage = (e) => console.log(e.data);
20
+ * postToWorker(worker, { id: 1, args: [42] });
21
+ * ```
22
+ *
23
+ * @module worker-factory
24
+ */
25
+
26
+ import { env } from './env.js';
27
+
28
+ // ---------------------------------------------------------------------------
29
+ // Worker creation
30
+ // ---------------------------------------------------------------------------
31
+
32
+ /**
33
+ * Create a platform-appropriate Worker from a script string.
34
+ *
35
+ * **Browser:** Creates a Blob URL and spawns a standard Web Worker.
36
+ * **Node.js/Bun:** Uses `worker_threads.Worker` with the script as eval code.
37
+ * **Deno:** Uses Deno's Worker API with a Blob URL.
38
+ *
39
+ * @param {string} scriptSource - JavaScript source code for the worker.
40
+ * @param {Object} [options={}]
41
+ * @param {'classic'|'module'} [options.type='classic'] - Script module type.
42
+ * @param {string[]} [options.imports] - URLs to import before running the script.
43
+ * @returns {WorkerInterface} A worker with a unified interface.
44
+ * @throws {Error} If Worker creation fails in the current environment.
45
+ *
46
+ * @example
47
+ * ```js
48
+ * const worker = createWorker(`
49
+ * self.onmessage = function(e) {
50
+ * self.postMessage(e.data * 2);
51
+ * };
52
+ * `);
53
+ *
54
+ * worker.onmessage = (e) => console.log(e.data); // 84
55
+ * worker.postMessage(42);
56
+ * ```
57
+ */
58
+ export function createWorker(scriptSource, options = {}) {
59
+ const { type = 'classic', imports = [] } = options;
60
+
61
+ // --- Browser ---
62
+ if (env.isBrowser || (env.runtime === 'deno' && !env.isEdge)) {
63
+ return _createBrowserWorker(scriptSource, { type, imports });
64
+ }
65
+
66
+ // --- Node.js / Bun ---
67
+ if (env.isNode || env.isBun) {
68
+ return _createNodeWorker(scriptSource, { type, imports });
69
+ }
70
+
71
+ // --- Deno (if not caught above) ---
72
+ if (env.isDeno) {
73
+ return _createDenoWorker(scriptSource, { type, imports });
74
+ }
75
+
76
+ throw new Error(
77
+ `thread: Worker creation is not supported in this environment (${env.runtime}/${env.context}). ` +
78
+ 'Supported: browser, Node.js, Bun, Deno.'
79
+ );
80
+ }
81
+
82
+ // ---------------------------------------------------------------------------
83
+ // Browser worker
84
+ // ---------------------------------------------------------------------------
85
+
86
+ /**
87
+ * @private
88
+ */
89
+ function _createBrowserWorker(scriptSource, { type, imports }) {
90
+ // Prepend importScripts calls if needed
91
+ let fullScript = scriptSource;
92
+ if (imports.length > 0 && type === 'classic') {
93
+ const importLines = imports.map((u) => `importScripts('${u}');`).join('\n');
94
+ fullScript = importLines + '\n' + scriptSource;
95
+ }
96
+
97
+ const blob = new Blob([fullScript], { type: 'application/javascript' });
98
+ const blobUrl = URL.createObjectURL(blob);
99
+
100
+ try {
101
+ const worker = new Worker(blobUrl, { type });
102
+ // Attach blob URL for later cleanup
103
+ worker._threadBlobUrl = blobUrl;
104
+ worker._threadType = 'browser';
105
+ return worker;
106
+ } catch (err) {
107
+ URL.revokeObjectURL(blobUrl);
108
+ throw err;
109
+ }
110
+ }
111
+
112
+ // ---------------------------------------------------------------------------
113
+ // Node.js / Bun worker
114
+ // ---------------------------------------------------------------------------
115
+
116
+ /**
117
+ * @private
118
+ */
119
+ function _createNodeWorker(scriptSource, { type, imports }) {
120
+ let WorkerClass;
121
+
122
+ // Try node:worker_threads first (Node.js with globalThis.require)
123
+ try {
124
+ if (typeof globalThis.require === 'function') {
125
+ const wt = globalThis.require('node:worker_threads');
126
+ WorkerClass = wt.Worker;
127
+ }
128
+ } catch { /* ignore */ }
129
+
130
+ // Fallback: try bare require (Bun exposes require but not on globalThis)
131
+ if (!WorkerClass) {
132
+ try {
133
+ if (typeof require === 'function') {
134
+ const wt = require('node:worker_threads');
135
+ WorkerClass = wt.Worker;
136
+ }
137
+ } catch { /* ignore */ }
138
+ }
139
+
140
+ if (!WorkerClass) {
141
+ throw new Error('thread: worker_threads module is not available');
142
+ }
143
+
144
+ // Build import preamble for Node workers
145
+ let fullScript = scriptSource;
146
+ if (imports.length > 0) {
147
+ const importLines = imports.map((u) => `importScripts('${u}');`).join('\n');
148
+ fullScript = importLines + '\n' + scriptSource;
149
+ }
150
+
151
+ // Node worker_threads expects eval or a file path
152
+ // We use eval mode with the script as the worker code
153
+ const worker = new WorkerClass(fullScript, {
154
+ eval: true,
155
+ // Node.js worker_threads needs these for message compatibility
156
+ stdout: false,
157
+ stderr: false,
158
+ });
159
+
160
+ // Wrap to match browser Worker API
161
+ const wrapped = _wrapNodeWorker(worker);
162
+ wrapped._threadType = 'node';
163
+ return wrapped;
164
+ }
165
+
166
+ // ---------------------------------------------------------------------------
167
+ // Deno worker
168
+ // ---------------------------------------------------------------------------
169
+
170
+ /**
171
+ * @private
172
+ */
173
+ function _createDenoWorker(scriptSource, { type, imports }) {
174
+ let fullScript = scriptSource;
175
+ if (imports.length > 0) {
176
+ const importLines = imports.map((u) => `import '${u}';`).join('\n');
177
+ fullScript = importLines + '\n' + scriptSource;
178
+ }
179
+
180
+ const blob = new Blob([fullScript], { type: 'application/javascript' });
181
+ const blobUrl = URL.createObjectURL(blob);
182
+
183
+ try {
184
+ const worker = new Worker(blobUrl, { type: 'module' });
185
+ worker._threadBlobUrl = blobUrl;
186
+ worker._threadType = 'deno';
187
+ return worker;
188
+ } catch (err) {
189
+ URL.revokeObjectURL(blobUrl);
190
+ throw err;
191
+ }
192
+ }
193
+
194
+ // ---------------------------------------------------------------------------
195
+ // Node.js worker_threads → browser Worker API adapter
196
+ // ---------------------------------------------------------------------------
197
+
198
+ /**
199
+ * Wrap a Node.js `worker_threads.Worker` to expose the browser Worker API.
200
+ *
201
+ * Node's worker_threads uses a different event model (EventEmitter).
202
+ * This wrapper translates `.on('message', ...)` to `.onmessage = ...`.
203
+ *
204
+ * @param {import('worker_threads').Worker} nodeWorker
205
+ * @returns {WorkerInterface} Browser-compatible Worker wrapper.
206
+ * @private
207
+ */
208
+ function _wrapNodeWorker(nodeWorker) {
209
+ const wrapped = {
210
+ _nodeWorker: nodeWorker,
211
+ _threadType: 'node',
212
+ _onmessage: null,
213
+ _onerror: null,
214
+ _onmessageerror: null,
215
+
216
+ get onmessage() { return this._onmessage; },
217
+ set onmessage(fn) {
218
+ this._onmessage = fn;
219
+ // Node worker_threads uses 'message' event
220
+ nodeWorker.removeAllListeners('message');
221
+ if (fn) {
222
+ nodeWorker.on('message', (data) => {
223
+ // Wrap in a MessageEvent-like object
224
+ fn({ data });
225
+ });
226
+ }
227
+ },
228
+
229
+ get onerror() { return this._onerror; },
230
+ set onerror(fn) {
231
+ this._onerror = fn;
232
+ nodeWorker.removeAllListeners('error');
233
+ if (fn) {
234
+ nodeWorker.on('error', (err) => {
235
+ fn({ message: err.message, error: err, preventDefault() {} });
236
+ });
237
+ }
238
+ },
239
+
240
+ get onmessageerror() { return this._onmessageerror; },
241
+ set onmessageerror(fn) {
242
+ this._onmessageerror = fn;
243
+ // Node doesn't have messageerror — no-op
244
+ },
245
+
246
+ postMessage(data, transfer) {
247
+ // Node worker_threads postMessage
248
+ if (transfer && Array.isArray(transfer)) {
249
+ nodeWorker.postMessage(data, transfer);
250
+ } else {
251
+ nodeWorker.postMessage(data);
252
+ }
253
+ },
254
+
255
+ terminate() {
256
+ try { nodeWorker.terminate(); } catch { /* already terminated */ }
257
+ },
258
+
259
+ // Bonus: Node-specific APIs for advanced usage
260
+ get threadId() {
261
+ return nodeWorker.threadId;
262
+ },
263
+ };
264
+
265
+ // Proxy any other method calls to the underlying worker
266
+ return new Proxy(wrapped, {
267
+ get(target, prop) {
268
+ if (prop in target) return target[prop];
269
+ // Delegate to the underlying worker for unknown properties
270
+ const val = nodeWorker[prop];
271
+ return typeof val === 'function' ? val.bind(nodeWorker) : val;
272
+ },
273
+ });
274
+ }
275
+
276
+ // ---------------------------------------------------------------------------
277
+ // Worker utilities
278
+ // ---------------------------------------------------------------------------
279
+
280
+ /**
281
+ * Post a message to a worker, handling platform differences.
282
+ *
283
+ * @param {WorkerInterface} worker - The worker to send to.
284
+ * @param {*} data - Message data.
285
+ * @param {Transferable[]} [transfer=[]] - Transferable objects.
286
+ */
287
+ export function postToWorker(worker, data, transfer = []) {
288
+ if (worker._threadType === 'node') {
289
+ worker.postMessage(data, transfer);
290
+ } else {
291
+ worker.postMessage(data, transfer);
292
+ }
293
+ }
294
+
295
+ /**
296
+ * Terminate a worker and clean up resources.
297
+ *
298
+ * Revokes Blob URLs (browser/Deno) and terminates the worker.
299
+ *
300
+ * @param {WorkerInterface} worker - The worker to terminate.
301
+ */
302
+ export function terminateWorker(worker) {
303
+ // Revoke Blob URL if applicable
304
+ if (worker._threadBlobUrl) {
305
+ try { URL.revokeObjectURL(worker._threadBlobUrl); } catch { /* ignore */ }
306
+ }
307
+ worker.terminate();
308
+ }
309
+
310
+ /**
311
+ * Check if the current environment supports Worker creation.
312
+ *
313
+ * @returns {boolean} `true` if Workers can be created.
314
+ */
315
+ export function supportsWorkers() {
316
+ return env.hasWorker || env.isNode || env.isBun || env.isDeno;
317
+ }
318
+
319
+ /**
320
+ * Get a description of the current Worker support.
321
+ *
322
+ * @returns {{ supported: boolean, type: string, details: string }}
323
+ *
324
+ * @example
325
+ * ```js
326
+ * const info = workerInfo();
327
+ * console.log(info); // { supported: true, type: 'browser', details: 'Web Workers via Blob URLs' }
328
+ * ```
329
+ */
330
+ export function workerInfo() {
331
+ if (env.isBrowser) {
332
+ return { supported: true, type: 'browser', details: 'Web Workers via Blob URLs' };
333
+ }
334
+ if (env.isBun) {
335
+ return { supported: true, type: 'bun', details: 'Bun worker_threads (eval mode)' };
336
+ }
337
+ if (env.isNode) {
338
+ return { supported: true, type: 'node', details: 'Node.js worker_threads (eval mode)' };
339
+ }
340
+ if (env.isDeno) {
341
+ return { supported: true, type: 'deno', details: 'Deno Workers via Blob URLs' };
342
+ }
343
+ return { supported: false, type: 'none', details: 'No Worker support detected' };
344
+ }
345
+
346
+ // Re-export env for convenience
347
+ export { env } from './env.js';