@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.
- package/LICENSE +21 -0
- package/README.md +1156 -0
- package/package.json +133 -0
- package/src/adapters.js +404 -0
- package/src/config/adapters.js +188 -0
- package/src/config/define.js +171 -0
- package/src/config/env.js +218 -0
- package/src/config/frameworks.js +268 -0
- package/src/config/index.js +256 -0
- package/src/config/schema.js +375 -0
- package/src/config.js +52 -0
- package/src/deno.js +29 -0
- package/src/edge.js +38 -0
- package/src/env.js +361 -0
- package/src/error.js +340 -0
- package/src/factory.js +591 -0
- package/src/gpu/adapters.js +227 -0
- package/src/gpu/chains.js +261 -0
- package/src/gpu/env.js +372 -0
- package/src/gpu/gpu.js +1199 -0
- package/src/gpu/helpers.js +240 -0
- package/src/gpu/hooks.js +284 -0
- package/src/gpu/index.js +16 -0
- package/src/gpu/shaders.js +834 -0
- package/src/gpu/special.js +493 -0
- package/src/hooks.js +448 -0
- package/src/index.js +557 -0
- package/src/metrix.js +222 -0
- package/src/node.js +36 -0
- package/src/pool.js +740 -0
- package/src/serializer.js +139 -0
- package/src/thread.js +1246 -0
- package/src/types.js +1354 -0
- package/src/worker-factory.js +347 -0
package/src/env.js
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Environment detection for the thread library.
|
|
3
|
+
*
|
|
4
|
+
* Detects the current JavaScript runtime and exposes flags for
|
|
5
|
+
* conditional code paths. Used internally by Thread, GPUCompute,
|
|
6
|
+
* and the config loader to pick the right APIs.
|
|
7
|
+
*
|
|
8
|
+
* Supported environments:
|
|
9
|
+
* - **Browser** — Web Workers, Blob URLs, `navigator.gpu`
|
|
10
|
+
* - **Node.js** — `worker_threads`, `node:worker_threads`
|
|
11
|
+
* - **Bun** — `bun:ffi`, `node:worker_threads` (Bun's built-in)
|
|
12
|
+
* - **Deno** — `Deno.Worker`, `Deno.openKv`, `navigator.gpu`
|
|
13
|
+
* - **Cloudflare Workers** — `workers-runtime` (no `Worker` constructor)
|
|
14
|
+
* - **Edge runtimes** — Vercel Edge, Netlify Edge, Deno Deploy
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```js
|
|
18
|
+
* import { env } from './env.js';
|
|
19
|
+
*
|
|
20
|
+
* if (env.isNode) {
|
|
21
|
+
* const { Worker } = await import('node:worker_threads');
|
|
22
|
+
* } else if (env.isBrowser) {
|
|
23
|
+
* const worker = new Worker(url);
|
|
24
|
+
* }
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* @module env
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Runtime detection
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @type {'browser'|'node'|'bun'|'deno'|'edge'|'unknown'}
|
|
36
|
+
* Detected runtime identifier.
|
|
37
|
+
*/
|
|
38
|
+
const runtime = (() => {
|
|
39
|
+
// Bun — check first because Bun also satisfies Node checks
|
|
40
|
+
if (typeof globalThis.Bun !== 'undefined') return 'bun';
|
|
41
|
+
// Deno
|
|
42
|
+
if (typeof globalThis.Deno !== 'undefined') return 'deno';
|
|
43
|
+
// Node.js
|
|
44
|
+
if (typeof globalThis.process !== 'undefined' && typeof globalThis.process.versions?.node !== 'undefined') return 'node';
|
|
45
|
+
// Browser (including Web Workers, Service Workers)
|
|
46
|
+
if (typeof globalThis.navigator !== 'undefined' || typeof globalThis.self !== 'undefined') {
|
|
47
|
+
// Cloudflare Workers / Vercel Edge — no full `Worker` constructor
|
|
48
|
+
if (typeof globalThis.caches !== 'undefined' && typeof globalThis.XMLHttpRequest === 'undefined') return 'edge';
|
|
49
|
+
return 'browser';
|
|
50
|
+
}
|
|
51
|
+
// Edge runtime fallback
|
|
52
|
+
if (typeof globalThis.EdgeRuntime !== 'undefined') return 'edge';
|
|
53
|
+
return 'unknown';
|
|
54
|
+
})();
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @type {'main'|'worker'|'service-worker'|'unknown'}
|
|
58
|
+
* Current execution context.
|
|
59
|
+
*/
|
|
60
|
+
const context = (() => {
|
|
61
|
+
// Service Worker
|
|
62
|
+
if (typeof ServiceWorkerGlobalScope !== 'undefined' && globalThis instanceof ServiceWorkerGlobalScope) return 'service-worker';
|
|
63
|
+
// Web Worker / Deno Worker / Node worker_threads
|
|
64
|
+
if (typeof DedicatedWorkerGlobalScope !== 'undefined' && globalThis instanceof DedicatedWorkerGlobalScope) return 'worker';
|
|
65
|
+
if (typeof SharedWorkerGlobalScope !== 'undefined' && globalThis instanceof SharedWorkerGlobalScope) return 'worker';
|
|
66
|
+
// Node.js worker_threads
|
|
67
|
+
if (typeof globalThis.process !== 'undefined' && globalThis.process.env?.IS_WORKER_THREAD === '1') return 'worker';
|
|
68
|
+
// Bun worker_threads
|
|
69
|
+
if (typeof globalThis.Bun !== 'undefined' && typeof globalThis.Bun?.sleep === 'function' && typeof globalThis.self === 'undefined') return 'worker';
|
|
70
|
+
// Main thread
|
|
71
|
+
if (typeof window !== 'undefined' || typeof globalThis.window !== 'undefined') return 'main';
|
|
72
|
+
if (typeof globalThis.process !== 'undefined' && typeof globalThis.process.argv !== 'undefined') return 'main';
|
|
73
|
+
if (typeof globalThis.Deno !== 'undefined') return 'main';
|
|
74
|
+
return 'unknown';
|
|
75
|
+
})();
|
|
76
|
+
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
// Feature detection (cached)
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
/** @type {boolean} `true` if the native `Worker` constructor is available. */
|
|
82
|
+
const hasWorker = (() => {
|
|
83
|
+
if (typeof Worker !== 'undefined') return true;
|
|
84
|
+
// Node.js worker_threads has Worker in newer versions
|
|
85
|
+
try {
|
|
86
|
+
if (typeof globalThis.require === 'function') {
|
|
87
|
+
const wt = globalThis.require('node:worker_threads');
|
|
88
|
+
return typeof wt.Worker === 'function';
|
|
89
|
+
}
|
|
90
|
+
} catch { /* ignore */ }
|
|
91
|
+
return false;
|
|
92
|
+
})();
|
|
93
|
+
|
|
94
|
+
/** @type {boolean} `true` if WebGPU (`navigator.gpu`) is available. */
|
|
95
|
+
const hasGPU = (() => {
|
|
96
|
+
if (typeof navigator !== 'undefined' && navigator.gpu) return true;
|
|
97
|
+
// Deno
|
|
98
|
+
if (typeof globalThis.Deno !== 'undefined' && globalThis.Deno?.gpu) return true;
|
|
99
|
+
// Bun — check built-in gpu API
|
|
100
|
+
if (typeof globalThis.Bun !== 'undefined' && globalThis.Bun?.gpu) return true;
|
|
101
|
+
return false;
|
|
102
|
+
})();
|
|
103
|
+
|
|
104
|
+
/** @type {boolean} `true` if `fs` module is available (Node/Bun). */
|
|
105
|
+
const hasFS = (() => {
|
|
106
|
+
if (typeof globalThis.require === 'function') {
|
|
107
|
+
try { globalThis.require('node:fs'); return true; } catch { return false; }
|
|
108
|
+
}
|
|
109
|
+
if (typeof globalThis.Bun !== 'undefined') return true;
|
|
110
|
+
return false;
|
|
111
|
+
})();
|
|
112
|
+
|
|
113
|
+
/** @type {boolean} `true` if `path` module is available (Node/Bun). */
|
|
114
|
+
const hasPath = (() => {
|
|
115
|
+
if (typeof globalThis.require === 'function') {
|
|
116
|
+
try { globalThis.require('node:path'); return true; } catch { return false; }
|
|
117
|
+
}
|
|
118
|
+
return false;
|
|
119
|
+
})();
|
|
120
|
+
|
|
121
|
+
/** @type {boolean} `true` if `performance.memory` is available (Chrome). */
|
|
122
|
+
const hasMemoryAPI = (() => {
|
|
123
|
+
return typeof performance !== 'undefined' && typeof performance.memory !== 'undefined';
|
|
124
|
+
})();
|
|
125
|
+
|
|
126
|
+
/** @type {boolean} `true` if Blob and URL.createObjectURL are available. */
|
|
127
|
+
const hasBlob = (() => {
|
|
128
|
+
return typeof Blob !== 'undefined' && typeof URL !== 'undefined' && typeof URL.createObjectURL === 'function';
|
|
129
|
+
})();
|
|
130
|
+
|
|
131
|
+
/** @type {boolean} `true` if dynamic `import()` is available. */
|
|
132
|
+
const hasDynamicImport = (() => {
|
|
133
|
+
try {
|
|
134
|
+
return typeof globalThis.import === 'function' || typeof Function('return import') === 'function';
|
|
135
|
+
} catch { return false; }
|
|
136
|
+
})();
|
|
137
|
+
|
|
138
|
+
/** @type {boolean} `true` if the runtime is Bun. */
|
|
139
|
+
const isBun = runtime === 'bun';
|
|
140
|
+
|
|
141
|
+
/** @type {boolean} `true` if the runtime is Node.js. */
|
|
142
|
+
const isNode = runtime === 'node';
|
|
143
|
+
|
|
144
|
+
/** @type {boolean} `true` if the runtime is Deno. */
|
|
145
|
+
const isDeno = runtime === 'deno';
|
|
146
|
+
|
|
147
|
+
/** @type {boolean} `true` if the runtime is a browser (including Web Workers). */
|
|
148
|
+
const isBrowser = runtime === 'browser';
|
|
149
|
+
|
|
150
|
+
/** @type {boolean} `true` if the runtime is an edge function. */
|
|
151
|
+
const isEdge = runtime === 'edge';
|
|
152
|
+
|
|
153
|
+
/** @type {boolean} `true` if running in a worker context (not main thread). */
|
|
154
|
+
const isWorker = context === 'worker';
|
|
155
|
+
|
|
156
|
+
/** @type {boolean} `true` if running on the main thread. */
|
|
157
|
+
const isMainThread = context === 'main';
|
|
158
|
+
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
// Platform-specific require/import helpers
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Safely require a Node.js built-in module.
|
|
165
|
+
*
|
|
166
|
+
* Returns the module if available, `null` otherwise. Works in Node.js,
|
|
167
|
+
* Bun, and environments where `globalThis.require` is defined.
|
|
168
|
+
*
|
|
169
|
+
* @param {string} moduleId - Module name (e.g. `'node:fs'`, `'node:path'`).
|
|
170
|
+
* @returns {any|null} The required module, or `null`.
|
|
171
|
+
*
|
|
172
|
+
* @example
|
|
173
|
+
* ```js
|
|
174
|
+
* const fs = requireModule('node:fs');
|
|
175
|
+
* if (fs) {
|
|
176
|
+
* const data = fs.readFileSync('config.json', 'utf-8');
|
|
177
|
+
* }
|
|
178
|
+
* ```
|
|
179
|
+
*/
|
|
180
|
+
function requireModule(moduleId) {
|
|
181
|
+
try {
|
|
182
|
+
if (typeof globalThis.require === 'function') {
|
|
183
|
+
return globalThis.require(moduleId);
|
|
184
|
+
}
|
|
185
|
+
} catch { /* ignore */ }
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Get the current working directory.
|
|
191
|
+
*
|
|
192
|
+
* Works across Node.js, Bun, Deno, and browser environments.
|
|
193
|
+
*
|
|
194
|
+
* @returns {string} The best-effort working directory.
|
|
195
|
+
*
|
|
196
|
+
* @example
|
|
197
|
+
* ```js
|
|
198
|
+
* const cwd = getCwd();
|
|
199
|
+
* console.log('Working from:', cwd);
|
|
200
|
+
* ```
|
|
201
|
+
*/
|
|
202
|
+
function getCwd() {
|
|
203
|
+
// Node.js / Bun
|
|
204
|
+
if (typeof process !== 'undefined' && typeof process.cwd === 'function') {
|
|
205
|
+
return process.cwd();
|
|
206
|
+
}
|
|
207
|
+
// Deno
|
|
208
|
+
if (typeof globalThis.Deno !== 'undefined' && typeof globalThis.Deno.cwd === 'function') {
|
|
209
|
+
return globalThis.Deno.cwd();
|
|
210
|
+
}
|
|
211
|
+
// Browser — use document.baseURI or location
|
|
212
|
+
if (typeof document !== 'undefined' && document.baseURI) {
|
|
213
|
+
try { return new URL(document.baseURI).pathname; } catch { /* ignore */ }
|
|
214
|
+
}
|
|
215
|
+
if (typeof location !== 'undefined' && location.href) {
|
|
216
|
+
try { return new URL(location.href).pathname; } catch { /* ignore */ }
|
|
217
|
+
}
|
|
218
|
+
return '.';
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Read a file synchronously.
|
|
223
|
+
*
|
|
224
|
+
* Uses `fs.readFileSync` in Node/Bun, `Deno.readTextFileSync` in Deno,
|
|
225
|
+
* or returns `null` in browser environments.
|
|
226
|
+
*
|
|
227
|
+
* @param {string} filePath - Absolute or relative file path.
|
|
228
|
+
* @returns {string|null} File contents, or `null` if not readable.
|
|
229
|
+
*
|
|
230
|
+
* @example
|
|
231
|
+
* ```js
|
|
232
|
+
* const content = readFileSync('./config.json');
|
|
233
|
+
* if (content) {
|
|
234
|
+
* const config = JSON.parse(content);
|
|
235
|
+
* }
|
|
236
|
+
* ```
|
|
237
|
+
*/
|
|
238
|
+
function readFileSync(filePath) {
|
|
239
|
+
// Node.js / Bun
|
|
240
|
+
const fs = requireModule('node:fs');
|
|
241
|
+
if (fs && typeof fs.readFileSync === 'function') {
|
|
242
|
+
return fs.readFileSync(filePath, 'utf-8');
|
|
243
|
+
}
|
|
244
|
+
// Deno
|
|
245
|
+
if (typeof globalThis.Deno !== 'undefined' && typeof globalThis.Deno.readTextFileSync === 'function') {
|
|
246
|
+
try { return globalThis.Deno.readTextFileSync(filePath); } catch { return null; }
|
|
247
|
+
}
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Check if a file exists.
|
|
253
|
+
*
|
|
254
|
+
* @param {string} filePath - Path to check.
|
|
255
|
+
* @returns {boolean} `true` if the file exists and is accessible.
|
|
256
|
+
*
|
|
257
|
+
* @example
|
|
258
|
+
* ```js
|
|
259
|
+
* if (fileExists('./thread.config.js')) {
|
|
260
|
+
* loadConfig();
|
|
261
|
+
* }
|
|
262
|
+
* ```
|
|
263
|
+
*/
|
|
264
|
+
function fileExists(filePath) {
|
|
265
|
+
// Node.js / Bun
|
|
266
|
+
const fs = requireModule('node:fs');
|
|
267
|
+
if (fs && typeof fs.existsSync === 'function') {
|
|
268
|
+
return fs.existsSync(filePath);
|
|
269
|
+
}
|
|
270
|
+
// Deno
|
|
271
|
+
if (typeof globalThis.Deno !== 'undefined' && typeof globalThis.Deno.statSync === 'function') {
|
|
272
|
+
try { globalThis.Deno.statSync(filePath); return true; } catch { return false; }
|
|
273
|
+
}
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Resolve a file path relative to the current working directory.
|
|
279
|
+
*
|
|
280
|
+
* @param {string} ...segments - Path segments to join.
|
|
281
|
+
* @returns {string} Resolved absolute path.
|
|
282
|
+
*
|
|
283
|
+
* @example
|
|
284
|
+
* ```js
|
|
285
|
+
* const configPath = resolvePath('thread.config.js');
|
|
286
|
+
* const fullPath = resolvePath('src', 'config', 'index.js');
|
|
287
|
+
* ```
|
|
288
|
+
*/
|
|
289
|
+
function resolvePath(...segments) {
|
|
290
|
+
// Node.js / Bun
|
|
291
|
+
const path = requireModule('node:path');
|
|
292
|
+
if (path) {
|
|
293
|
+
return path.resolve(getCwd(), ...segments);
|
|
294
|
+
}
|
|
295
|
+
// Deno
|
|
296
|
+
if (typeof globalThis.Deno !== 'undefined' && typeof globalThis.Deno.resolve === 'function') {
|
|
297
|
+
return globalThis.Deno.resolve(...segments);
|
|
298
|
+
}
|
|
299
|
+
// Fallback — simple join
|
|
300
|
+
return [getCwd(), ...segments].join('/').replace(/\/+/g, '/');
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// ---------------------------------------------------------------------------
|
|
304
|
+
// Exports
|
|
305
|
+
// ---------------------------------------------------------------------------
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Comprehensive environment information for the thread library.
|
|
309
|
+
*
|
|
310
|
+
* Provides runtime detection, feature flags, and platform-specific
|
|
311
|
+
* helpers for cross-environment compatibility.
|
|
312
|
+
*
|
|
313
|
+
* @type {{
|
|
314
|
+
* runtime: 'browser'|'node'|'bun'|'deno'|'edge'|'unknown',
|
|
315
|
+
* context: 'main'|'worker'|'service-worker'|'unknown',
|
|
316
|
+
* isBrowser: boolean,
|
|
317
|
+
* isNode: boolean,
|
|
318
|
+
* isBun: boolean,
|
|
319
|
+
* isDeno: boolean,
|
|
320
|
+
* isEdge: boolean,
|
|
321
|
+
* isWorker: boolean,
|
|
322
|
+
* isMainThread: boolean,
|
|
323
|
+
* hasWorker: boolean,
|
|
324
|
+
* hasGPU: boolean,
|
|
325
|
+
* hasFS: boolean,
|
|
326
|
+
* hasPath: boolean,
|
|
327
|
+
* hasMemoryAPI: boolean,
|
|
328
|
+
* hasBlob: boolean,
|
|
329
|
+
* hasDynamicImport: boolean,
|
|
330
|
+
* requireModule: function(string): any|null,
|
|
331
|
+
* getCwd: function(): string,
|
|
332
|
+
* readFileSync: function(string): string|null,
|
|
333
|
+
* fileExists: function(string): boolean,
|
|
334
|
+
* resolvePath: function(...string): string
|
|
335
|
+
* }}
|
|
336
|
+
*/
|
|
337
|
+
export const env = Object.freeze({
|
|
338
|
+
runtime,
|
|
339
|
+
context,
|
|
340
|
+
isBrowser,
|
|
341
|
+
isNode,
|
|
342
|
+
isBun,
|
|
343
|
+
isDeno,
|
|
344
|
+
isEdge,
|
|
345
|
+
isWorker,
|
|
346
|
+
isMainThread,
|
|
347
|
+
hasWorker,
|
|
348
|
+
hasGPU,
|
|
349
|
+
hasFS,
|
|
350
|
+
hasPath,
|
|
351
|
+
hasMemoryAPI,
|
|
352
|
+
hasBlob,
|
|
353
|
+
hasDynamicImport,
|
|
354
|
+
requireModule,
|
|
355
|
+
getCwd,
|
|
356
|
+
readFileSync,
|
|
357
|
+
fileExists,
|
|
358
|
+
resolvePath,
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
export default env;
|
package/src/error.js
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Error hierarchy for the thread module.
|
|
3
|
+
*
|
|
4
|
+
* Every error in the thread system extends {@link ThreadError}, so a single
|
|
5
|
+
* `catch (e) { if (e instanceof ThreadError) }` block handles them all.
|
|
6
|
+
* Use the more specific subclasses to differentiate timeout, abort,
|
|
7
|
+
* termination, health, and dependency failures.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```js
|
|
11
|
+
* import {
|
|
12
|
+
* ThreadError,
|
|
13
|
+
* ThreadTimeoutError,
|
|
14
|
+
* ThreadAbortError,
|
|
15
|
+
* ThreadTerminatedError,
|
|
16
|
+
* ThreadHealthError,
|
|
17
|
+
* ThreadDependencyError,
|
|
18
|
+
* } from './error.js';
|
|
19
|
+
*
|
|
20
|
+
* try {
|
|
21
|
+
* await thread.run(hugeDataset, { timeout: 5000 });
|
|
22
|
+
* } catch (err) {
|
|
23
|
+
* if (err instanceof ThreadTimeoutError) {
|
|
24
|
+
* console.warn('Task took too long – retrying with more time');
|
|
25
|
+
* } else if (err instanceof ThreadAbortError) {
|
|
26
|
+
* console.log('User cancelled – cleaning up');
|
|
27
|
+
* } else if (err instanceof ThreadTerminatedError) {
|
|
28
|
+
* console.error('Thread was killed');
|
|
29
|
+
* } else if (err instanceof ThreadError) {
|
|
30
|
+
* console.error('Worker error:', err.message);
|
|
31
|
+
* } else {
|
|
32
|
+
* throw err; // re-throw non-thread errors
|
|
33
|
+
* }
|
|
34
|
+
* }
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* @module error
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// ThreadError – base class
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Base error class for all thread-related issues.
|
|
46
|
+
*
|
|
47
|
+
* This is the **parent** of every error the thread module can throw.
|
|
48
|
+
* Catching `ThreadError` lets you handle any thread-specific failure in
|
|
49
|
+
* one place; catching the subclasses lets you respond to specific failure
|
|
50
|
+
* modes.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```js
|
|
54
|
+
* import { ThreadError } from './error.js';
|
|
55
|
+
*
|
|
56
|
+
* // Generic catch-all
|
|
57
|
+
* try {
|
|
58
|
+
* await thread.run(data);
|
|
59
|
+
* } catch (err) {
|
|
60
|
+
* if (err instanceof ThreadError) {
|
|
61
|
+
* // any thread error – safe to handle generically
|
|
62
|
+
* console.error(`[thread] ${err.name}: ${err.message}`);
|
|
63
|
+
* }
|
|
64
|
+
* }
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
export class ThreadError extends Error {
|
|
68
|
+
/**
|
|
69
|
+
* @param {string} message - Human-readable description of the error.
|
|
70
|
+
*/
|
|
71
|
+
constructor(message) {
|
|
72
|
+
super(message);
|
|
73
|
+
/** @readonly @type {'ThreadError'} */
|
|
74
|
+
this.name = 'ThreadError';
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// ThreadTimeoutError
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Thrown when a task exceeds its allowed execution time.
|
|
84
|
+
*
|
|
85
|
+
* Every `thread.run()` and `pool.run()` accepts a `timeout` option (in ms).
|
|
86
|
+
* If the worker does not respond within that window the task promise is
|
|
87
|
+
* rejected with this error.
|
|
88
|
+
*
|
|
89
|
+
* **Common causes:**
|
|
90
|
+
* - The worker function has an infinite loop or is blocking on I/O.
|
|
91
|
+
* - The task is genuinely slow and needs a longer timeout.
|
|
92
|
+
* - The worker crashed before it could respond (the error message will
|
|
93
|
+
* include the timeout value).
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* ```js
|
|
97
|
+
* import { createThread, ThreadTimeoutError } from './index.js';
|
|
98
|
+
*
|
|
99
|
+
* const t = createThread((n) => {
|
|
100
|
+
* // Simulate a slow task
|
|
101
|
+
* const end = Date.now() + n;
|
|
102
|
+
* while (Date.now() < end) { busy_wait }
|
|
103
|
+
* return 'done';
|
|
104
|
+
* });
|
|
105
|
+
*
|
|
106
|
+
* try {
|
|
107
|
+
* const result = await t.run(60_000, { timeout: 5000 });
|
|
108
|
+
* } catch (err) {
|
|
109
|
+
* if (err instanceof ThreadTimeoutError) {
|
|
110
|
+
* console.error('Task exceeded 5s timeout:', err.message);
|
|
111
|
+
* // "Timed out after 5000ms"
|
|
112
|
+
* }
|
|
113
|
+
* }
|
|
114
|
+
* ```
|
|
115
|
+
*/
|
|
116
|
+
export class ThreadTimeoutError extends ThreadError {
|
|
117
|
+
/**
|
|
118
|
+
* @param {string} message - Timeout description (usually includes ms).
|
|
119
|
+
*/
|
|
120
|
+
constructor(message) {
|
|
121
|
+
super(message);
|
|
122
|
+
/** @readonly @type {'ThreadTimeoutError'} */
|
|
123
|
+
this.name = 'ThreadTimeoutError';
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
// ThreadAbortError
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Thrown when a task is cancelled via an `AbortController` or the pool's
|
|
133
|
+
* `cancel()` method.
|
|
134
|
+
*
|
|
135
|
+
* There are two ways to abort a task:
|
|
136
|
+
* 1. **AbortController** – pass `signal` in run options.
|
|
137
|
+
* 2. **Pool cancel** – call `pool.cancel(taskId)`.
|
|
138
|
+
*
|
|
139
|
+
* In both cases the task promise rejects with this error.
|
|
140
|
+
*
|
|
141
|
+
* @example
|
|
142
|
+
* ```js
|
|
143
|
+
* import { createThread, ThreadAbortError } from './index.js';
|
|
144
|
+
*
|
|
145
|
+
* const t = createThread((data) => process(data));
|
|
146
|
+
* const controller = new AbortController();
|
|
147
|
+
*
|
|
148
|
+
* // Start a long-running task
|
|
149
|
+
* const promise = t.run(hugeData, { signal: controller.signal });
|
|
150
|
+
*
|
|
151
|
+
* // User clicks "Cancel"
|
|
152
|
+
* document.getElementById('cancel').onclick = () => controller.abort();
|
|
153
|
+
*
|
|
154
|
+
* try {
|
|
155
|
+
* await promise;
|
|
156
|
+
* } catch (err) {
|
|
157
|
+
* if (err instanceof ThreadAbortError) {
|
|
158
|
+
* console.log('Task was cancelled by user');
|
|
159
|
+
* }
|
|
160
|
+
* }
|
|
161
|
+
* ```
|
|
162
|
+
*/
|
|
163
|
+
export class ThreadAbortError extends ThreadError {
|
|
164
|
+
/**
|
|
165
|
+
* @param {string} message - Abort description.
|
|
166
|
+
*/
|
|
167
|
+
constructor(message) {
|
|
168
|
+
super(message);
|
|
169
|
+
/** @readonly @type {'ThreadAbortError'} */
|
|
170
|
+
this.name = 'ThreadAbortError';
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
// ThreadTerminatedError
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Thrown when you try to use a thread that has been terminated, or when
|
|
180
|
+
* the pool shuts down while tasks are still queued.
|
|
181
|
+
*
|
|
182
|
+
* Once `thread.terminate()` or `pool.terminateAll()` is called, every
|
|
183
|
+
* pending and future task promise rejects with this error.
|
|
184
|
+
*
|
|
185
|
+
* @example
|
|
186
|
+
* ```js
|
|
187
|
+
* import { createThread, ThreadTerminatedError } from './index.js';
|
|
188
|
+
*
|
|
189
|
+
* const t = createThread((x) => x * 2);
|
|
190
|
+
* t.terminate();
|
|
191
|
+
*
|
|
192
|
+
* try {
|
|
193
|
+
* await t.run(5);
|
|
194
|
+
* } catch (err) {
|
|
195
|
+
* if (err instanceof ThreadTerminatedError) {
|
|
196
|
+
* console.error('Cannot use a terminated thread');
|
|
197
|
+
* }
|
|
198
|
+
* }
|
|
199
|
+
* ```
|
|
200
|
+
*/
|
|
201
|
+
export class ThreadTerminatedError extends ThreadError {
|
|
202
|
+
/**
|
|
203
|
+
* @param {string} message - Termination description.
|
|
204
|
+
*/
|
|
205
|
+
constructor(message) {
|
|
206
|
+
super(message);
|
|
207
|
+
/** @readonly @type {'ThreadTerminatedError'} */
|
|
208
|
+
this.name = 'ThreadTerminatedError';
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
// ThreadHealthError
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Thrown when a health check fails.
|
|
218
|
+
*
|
|
219
|
+
* When `healthCheckInterval` is set, the thread periodically pings the
|
|
220
|
+
* worker. If the worker does not respond within `healthCheckTimeout`,
|
|
221
|
+
* or if the ping throws, the thread considers the worker dead and emits
|
|
222
|
+
* this error.
|
|
223
|
+
*
|
|
224
|
+
* In practice this error is **not** thrown to your task promises. Instead
|
|
225
|
+
* it is logged internally and triggers an automatic restart. You will
|
|
226
|
+
* see it if you listen to the `error` event:
|
|
227
|
+
*
|
|
228
|
+
* @example
|
|
229
|
+
* ```js
|
|
230
|
+
* import { createThread, ThreadHealthError } from './index.js';
|
|
231
|
+
*
|
|
232
|
+
* const t = createThread((x) => x, {
|
|
233
|
+
* healthCheckInterval: 5000,
|
|
234
|
+
* healthCheckTimeout: 2000,
|
|
235
|
+
* });
|
|
236
|
+
*
|
|
237
|
+
* t.on('error', (info) => {
|
|
238
|
+
* if (info.error instanceof ThreadHealthError) {
|
|
239
|
+
* console.warn('Worker is unhealthy, restarting...');
|
|
240
|
+
* }
|
|
241
|
+
* });
|
|
242
|
+
* ```
|
|
243
|
+
*/
|
|
244
|
+
export class ThreadHealthError extends ThreadError {
|
|
245
|
+
/**
|
|
246
|
+
* @param {string} message - Health check failure description.
|
|
247
|
+
*/
|
|
248
|
+
constructor(message) {
|
|
249
|
+
super(message);
|
|
250
|
+
/** @readonly @type {'ThreadHealthError'} */
|
|
251
|
+
this.name = 'ThreadHealthError';
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ---------------------------------------------------------------------------
|
|
256
|
+
// ThreadDependencyError
|
|
257
|
+
// ---------------------------------------------------------------------------
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Thrown when a pool task's dependency fails.
|
|
261
|
+
*
|
|
262
|
+
* When you submit a task with `dependsOn: [taskId]` and the dependency
|
|
263
|
+
* task fails (for any reason), the dependent task is automatically
|
|
264
|
+
* rejected with this error instead of running.
|
|
265
|
+
*
|
|
266
|
+
* The error message includes the failed dependency's ID and the original
|
|
267
|
+
* error message so you can trace the failure chain.
|
|
268
|
+
*
|
|
269
|
+
* @example
|
|
270
|
+
* ```js
|
|
271
|
+
* import { createPool, ThreadDependencyError } from './index.js';
|
|
272
|
+
*
|
|
273
|
+
* const pool = createPool(2, (x) => {
|
|
274
|
+
* if (x < 0) throw new Error('Negative input');
|
|
275
|
+
* return x * 2;
|
|
276
|
+
* });
|
|
277
|
+
*
|
|
278
|
+
* const a = pool.run(-1); // will fail: "Negative input"
|
|
279
|
+
* const b = pool.run(10, {
|
|
280
|
+
* dependsOn: [a.id], // depends on 'a'
|
|
281
|
+
* });
|
|
282
|
+
*
|
|
283
|
+
* try {
|
|
284
|
+
* await b.promise;
|
|
285
|
+
* } catch (err) {
|
|
286
|
+
* if (err instanceof ThreadDependencyError) {
|
|
287
|
+
* // "Dependency task 0 failed: Negative input"
|
|
288
|
+
* console.error(err.message);
|
|
289
|
+
* }
|
|
290
|
+
* }
|
|
291
|
+
* ```
|
|
292
|
+
*/
|
|
293
|
+
export class ThreadDependencyError extends ThreadError {
|
|
294
|
+
/**
|
|
295
|
+
* @param {string} message - Dependency failure description.
|
|
296
|
+
*/
|
|
297
|
+
constructor(message) {
|
|
298
|
+
super(message);
|
|
299
|
+
/** @readonly @type {'ThreadDependencyError'} */
|
|
300
|
+
this.name = 'ThreadDependencyError';
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ---------------------------------------------------------------------------
|
|
305
|
+
// GPUComputeError
|
|
306
|
+
// ---------------------------------------------------------------------------
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Thrown when a GPU compute operation fails.
|
|
310
|
+
*
|
|
311
|
+
* Covers all GPU-specific failures: shader compilation errors, buffer
|
|
312
|
+
* size violations, device loss, and dispatch failures. The error's
|
|
313
|
+
* `cause` property contains the original WebGPU error when available.
|
|
314
|
+
*
|
|
315
|
+
* @example
|
|
316
|
+
* ```js
|
|
317
|
+
* import { GPUComputeError } from './error.js';
|
|
318
|
+
*
|
|
319
|
+
* try {
|
|
320
|
+
* await gpu.compute({ inputs: {}, outputBuffers: {} });
|
|
321
|
+
* } catch (err) {
|
|
322
|
+
* if (err instanceof GPUComputeError) {
|
|
323
|
+
* console.error(`GPU failed: ${err.message}`);
|
|
324
|
+
* if (err.cause) console.error('Original:', err.cause);
|
|
325
|
+
* }
|
|
326
|
+
* }
|
|
327
|
+
* ```
|
|
328
|
+
*/
|
|
329
|
+
export class GPUComputeError extends ThreadError {
|
|
330
|
+
/**
|
|
331
|
+
* @param {string} message - Human-readable GPU error description.
|
|
332
|
+
* @param {Error} [cause] - Original WebGPU error (if any).
|
|
333
|
+
*/
|
|
334
|
+
constructor(message, cause = undefined) {
|
|
335
|
+
super(message);
|
|
336
|
+
/** @readonly @type {'GPUComputeError'} */
|
|
337
|
+
this.name = 'GPUComputeError';
|
|
338
|
+
if (cause !== undefined) this.cause = cause;
|
|
339
|
+
}
|
|
340
|
+
}
|