@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/src/factory.js ADDED
@@ -0,0 +1,591 @@
1
+ /**
2
+ * @file Factory functions for creating threads, pools, and GPU compute instances.
3
+ *
4
+ * Factory functions provide a convenient, validated alternative to
5
+ * directly instantiating `new Thread(...)`, `new ThreadPool(...)`, or
6
+ * `new GPUCompute(...)`. They perform input validation, apply sensible
7
+ * defaults, and return ready-to-use instances.
8
+ *
9
+ * **When to use factories vs constructors:**
10
+ *
11
+ * | Situation | Use |
12
+ * |-----------------------------------------|----------------------------|
13
+ * | Need full control / subclassing | `new Thread(...)` |
14
+ * | Want validation + ergonomic API | `createThread()` |
15
+ * | Need a pool with defaults | `createPool()` |
16
+ * | One-liner fire-and-forget worker | `createWorker()` |
17
+ * | Repeatable worker from definition | `createWorkerDef()` |
18
+ * | Thread with auto error handling | `createManagedThread()` |
19
+ * | GPU with a single JS-defined op | `createGPUOp()` |
20
+ * | GPU with multiple pre-registered ops | `createGPUPipeline()` |
21
+ * | GPU ready for reductions | `createGPUReducer()` |
22
+ * | GPU with CPU fallback | `createGPUWithFallback()` |
23
+ *
24
+ * @example
25
+ * ```js
26
+ * import {
27
+ * createThread, createPool,
28
+ * createGPUOp, createGPUPipeline, createGPUReducer,
29
+ * } from './factory.js';
30
+ *
31
+ * // --- CPU workers ---
32
+ * const t = createThread((x) => x * 2);
33
+ * console.log(await t.run(5)); // 10
34
+ *
35
+ * const pool = createPool(4, heavyComputation, { autoRestart: true });
36
+ *
37
+ * // --- GPU compute ---
38
+ * const gpu = createGPUOp('double', (data) => data.value * 2);
39
+ * const result = await gpu.run('double', { inputs: { data: new Float32Array([1, 2, 3]) } });
40
+ *
41
+ * // GPU with multiple ops
42
+ * const pipeline = createGPUPipeline([
43
+ * ['ema', (data, { alpha }) => data.value * alpha + ...],
44
+ * ['squared', (data) => data.value * data.value],
45
+ * ]);
46
+ *
47
+ * // GPU ready for reductions
48
+ * const reducer = createGPUReducer();
49
+ * const { result } = await reducer.run('reduce_sum', { inputs: { data: someFloat32 } });
50
+ * ```
51
+ *
52
+ * @module factory
53
+ */
54
+
55
+ import { ThreadPool } from "./pool";
56
+ import { Thread } from "./thread";
57
+ import { GPUCompute } from "./gpu/gpu.js";
58
+
59
+ // ---------------------------------------------------------------------------
60
+ // createThread
61
+ // ---------------------------------------------------------------------------
62
+
63
+ /**
64
+ * Create a validated {@link Thread} instance.
65
+ *
66
+ * This is the **recommended** way to create threads. It validates the
67
+ * definition and options, applies sensible defaults, and returns a
68
+ * fully-initialised thread.
69
+ *
70
+ * @param {import('./types.js').ThreadDefinition | Function} definition
71
+ * A plain function (used as `exec`) or a `{ setup?, exec, cleanup? }`
72
+ * object.
73
+ * @param {import('./types.js').ThreadOptions} [options={}]
74
+ * Configuration for timeouts, health checks, event hooks, etc.
75
+ * @returns {Thread} A ready-to-use thread.
76
+ *
77
+ * @throws {TypeError} If `definition` is not a function or a valid
78
+ * `{ exec }` object.
79
+ *
80
+ *
81
+ * ```js
82
+ * // Minimal – just a function
83
+ * const t = createThread((a, b) => a + b);
84
+ * await t.run(3, 4); // 7
85
+ * ```
86
+ *
87
+ * ```js
88
+ * // Full-featured – with options
89
+ * const t = createThread((data, ctx) => {
90
+ * ctx.log('Processing...');
91
+ * ctx.reportProgress(0.5);
92
+ * return data.map((x) => x * 2);
93
+ * }, {
94
+ * timeout: 10_000,
95
+ * concurrency: 2,
96
+ * onLog: (msg) => console.log('[worker]', msg),
97
+ * onTiming: (ms) => console.log(`Done in ${ms.toFixed(1)}ms`),
98
+ * });
99
+ *
100
+ * const result = await t.run([1, 2, 3]);
101
+ * // [2, 4, 6]
102
+ * ```
103
+ *
104
+ * @example
105
+ * ```js
106
+ * // TypeScript – full type safety via JSDoc
107
+ * // @type {import('./types.js').ThreadDefinition}
108
+ * const def = {
109
+ * setup() { return { count: 0 }; },
110
+ * exec(state, delta) {
111
+ * state.count += delta;
112
+ * return state.count;
113
+ * },
114
+ * };
115
+ * const t = createThread(def, { timeout: 5000 });
116
+ * ```
117
+ */
118
+ export function createThread(definition, options = {}) {
119
+ return new Thread(definition, options);
120
+ }
121
+
122
+ // ---------------------------------------------------------------------------
123
+ // createPool
124
+ // ---------------------------------------------------------------------------
125
+
126
+ /**
127
+ * Create a validated {@link ThreadPool} instance.
128
+ *
129
+ * @param {number} size
130
+ * Initial number of worker threads. Must be ≥ 1.
131
+ * @param {import('./types.js').ThreadDefinition | Function} definition
132
+ * Worker definition forwarded to each thread.
133
+ * @param {import('./types.js').PoolOptions} [options={}]
134
+ * Pool and thread configuration.
135
+ * @returns {ThreadPool} A ready-to-use pool.
136
+ *
137
+ * @throws {TypeError} If `size` is not a positive integer.
138
+ * @throws {TypeError} If `definition` is not a function or a valid
139
+ * `{ exec }` object.
140
+ *
141
+ * @example
142
+ * ```js
143
+ * // CPU-intensive work across 4 threads
144
+ * const pool = createPool(4, (n) => {
145
+ * let sum = 0;
146
+ * for (let i = 0; i < n; i++) sum += Math.sqrt(i);
147
+ * return sum;
148
+ * });
149
+ *
150
+ * const results = await Promise.all(
151
+ * Array.from({ length: 20 }, (_, i) => pool.run(1_000_000 + i).promise)
152
+ * );
153
+ * console.log('All done:', results.length);
154
+ * ```
155
+ *
156
+ * @example
157
+ * ```js
158
+ * // With affinity routing – same entity always goes to same thread
159
+ * const pool = createPool(4, processEntity, {
160
+ * keyHasher: (args) => String(args[0].entityId),
161
+ * });
162
+ *
163
+ * // Entity 42 always lands on the same thread → cache-friendly
164
+ * await pool.run({ entityId: 42, data: [...] });
165
+ * ```
166
+ *
167
+ * @example
168
+ * ```js
169
+ * // Pipeline with dependencies
170
+ * const pool = createPool(2, (x) => x);
171
+ *
172
+ * const step1 = pool.run('raw-data');
173
+ * const step2 = pool.run(step1.id, { dependsOn: [step1.id] });
174
+ * const step3 = pool.run(step2.id, { dependsOn: [step2.id] });
175
+ *
176
+ * console.log(await step3.promise); // 'raw-data'
177
+ * ```
178
+ */
179
+ export function createPool(size, definition, options = {}) {
180
+ if (!Number.isInteger(size) || size < 1) {
181
+ throw new TypeError(`size must be a positive integer, got ${size}`);
182
+ }
183
+ return new ThreadPool(size, definition, options);
184
+ }
185
+
186
+ // ---------------------------------------------------------------------------
187
+ // createWorker (convenience)
188
+ // ---------------------------------------------------------------------------
189
+
190
+ /**
191
+ * Create a single-purpose thread from a function with built-in error
192
+ * handling and logging.
193
+ *
194
+ * This is a convenience wrapper around {@link createThread} that:
195
+ * - Registers an `onError` listener that logs to the console.
196
+ * - Returns a thread that is "ready to use" with minimal boilerplate.
197
+ *
198
+ * Ideal for quick one-off tasks where you don't need fine-grained
199
+ * control.
200
+ *
201
+ * @param {Function} fn
202
+ * The function to execute in the worker. Receives the same arguments
203
+ * as `thread.run(...)`.
204
+ * @param {import('./types.js').ThreadOptions} [options={}]
205
+ * Additional options forwarded to `createThread`.
206
+ * @returns {Thread} A thread with error logging pre-configured.
207
+ *
208
+ * @example
209
+ * ```js
210
+ * import { createWorker } from './factory.js';
211
+ *
212
+ * const hasher = createWorker((data) => {
213
+ * // SHA-256 or whatever
214
+ * return crypto.subtle.digest('SHA-256', data);
215
+ * }, { timeout: 5000 });
216
+ *
217
+ * const hash = await hasher.run(heavyBuffer);
218
+ * hasher.terminate();
219
+ * ```
220
+ *
221
+ * @example
222
+ * ```js
223
+ * // Use in a script – errors are logged automatically
224
+ * const worker = createWorker((text) => text.toUpperCase());
225
+ * console.log(await worker.run('hello')); // "HELLO"
226
+ * ```
227
+ */
228
+ export function createWorker(fn, options = {}) {
229
+ if (typeof fn !== 'function') {
230
+ throw new TypeError(`createWorker expects a function, got ${typeof fn}`);
231
+ }
232
+ const opts = {
233
+ onError: (info) => console.error('[worker error]', info.error, info.stack),
234
+ ...options,
235
+ };
236
+ return createThread(fn, opts);
237
+ }
238
+
239
+ // ---------------------------------------------------------------------------
240
+ // createWorkerDef (reusable definition factory)
241
+ // ---------------------------------------------------------------------------
242
+
243
+ /**
244
+ * Create a reusable thread **definition** object from setup/exec/cleanup
245
+ * functions.
246
+ *
247
+ * The returned object can be passed to `createThread()` or
248
+ * `createPool()` multiple times, creating independent workers with the
249
+ * same logic. This is useful when you want to stamp out many identical
250
+ * workers without duplicating the definition.
251
+ *
252
+ * @param {Object} def
253
+ * @param {Function} [def.setup]
254
+ * Runs once when the worker starts. Return value becomes `state`.
255
+ * @param {Function} def.exec
256
+ * Runs on every task. Receives `(state, ...args, ctx)`.
257
+ * @param {Function} [def.cleanup]
258
+ * Runs once before termination.
259
+ * @returns {import('./types.js').ThreadDefinition} A definition object
260
+ * ready for use with `createThread` or `createPool`.
261
+ *
262
+ * @throws {TypeError} If `def.exec` is not a function.
263
+ *
264
+ * @example
265
+ * ```js
266
+ * import { createWorkerDef, createPool } from './factory.js';
267
+ *
268
+ * const imageProcessor = createWorkerDef({
269
+ * setup() {
270
+ * // load a WASM module, open a DB, etc.
271
+ * return { module: loadWasm() };
272
+ * },
273
+ * async exec(state, imageData, ctx) {
274
+ * ctx.log('Processing image...');
275
+ * const result = state.module.process(imageData);
276
+ * ctx.reportProgress(1);
277
+ * return result;
278
+ * },
279
+ * cleanup(state) {
280
+ * state.module.free();
281
+ * },
282
+ * });
283
+ *
284
+ * // Reuse the same definition across multiple pools or threads
285
+ * const pool = createPool(4, imageProcessor);
286
+ * const standalone = createThread(imageProcessor);
287
+ * ```
288
+ */
289
+ export function createWorkerDef(def) {
290
+ if (!def || typeof def !== 'object') {
291
+ throw new TypeError('createWorkerDef expects an object');
292
+ }
293
+ if (typeof def.exec !== 'function') {
294
+ throw new TypeError('def.exec must be a function');
295
+ }
296
+ return {
297
+ setup: def.setup || null,
298
+ exec: def.exec,
299
+ cleanup: def.cleanup || null,
300
+ };
301
+ }
302
+
303
+ // ---------------------------------------------------------------------------
304
+ // createManagedThread
305
+ // ---------------------------------------------------------------------------
306
+
307
+ /**
308
+ * Create a thread with automatic logging, error handling, and optional
309
+ * metrics collection.
310
+ *
311
+ * "Managed" means the thread comes with:
312
+ * - **Error logging** – errors are logged to the console.
313
+ * - **Timing** – every task's duration is logged.
314
+ * - **Metrics listener** – optional callback receives live metrics.
315
+ * - **Health checks** – enabled by default (every 10s).
316
+ *
317
+ * This is the recommended entry point when you want a thread that
318
+ * "just works" without manual event wiring.
319
+ *
320
+ * @param {import('./types.js').ThreadDefinition | Function} definition
321
+ * Worker definition (same as `createThread`).
322
+ * @param {Object} [options={}]
323
+ * @param {number} [options.timeout=30000]
324
+ * Task timeout in ms.
325
+ * @param {boolean} [options.healthChecks=true]
326
+ * Enable automatic health checks.
327
+ * @param {number} [options.healthCheckInterval=10000]
328
+ * Health check interval in ms.
329
+ * @param {Function} [options.onMetrics]
330
+ * Callback for live metrics snapshots.
331
+ * @param {Function} [options.onLog]
332
+ * Callback for worker log messages.
333
+ * @param {import('./types.js').ThreadOptions} [options.thread]
334
+ * Additional options forwarded to the Thread constructor.
335
+ * @returns {Thread} A fully configured thread.
336
+ *
337
+ * @example
338
+ * ```js
339
+ * import { createManagedThread } from './factory.js';
340
+ *
341
+ * const t = createManagedThread((x) => x * 2, {
342
+ * timeout: 5000,
343
+ * onMetrics: (snap) => {
344
+ * dashboard.update(snap); // live dashboard
345
+ * },
346
+ * });
347
+ *
348
+ * // Errors and timing are logged automatically
349
+ * await t.run(21); // logs "Task completed in 2.3ms"
350
+ * ```
351
+ *
352
+ * @example
353
+ * ```js
354
+ * // With stateful definition
355
+ * const t = createManagedThread({
356
+ * setup() { return { cache: new Map() }; },
357
+ * exec(state, key) {
358
+ * if (state.cache.has(key)) return state.cache.get(key);
359
+ * const val = expensiveCompute(key);
360
+ * state.cache.set(key, val);
361
+ * return val;
362
+ * },
363
+ * }, {
364
+ * timeout: 10_000,
365
+ * healthChecks: true,
366
+ * onMetrics: (snap) => console.log(`Pool avg: ${snap.avg}ms`),
367
+ * });
368
+ * ```
369
+ */
370
+ export function createManagedThread(definition, options = {}) {
371
+ const {
372
+ healthChecks = true,
373
+ healthCheckInterval = 10_000,
374
+ onMetrics = null,
375
+ onLog = null,
376
+ thread: extraThreadOpts = {},
377
+ ...rest
378
+ } = options;
379
+
380
+ const threadOpts = {
381
+ ...rest,
382
+ ...extraThreadOpts,
383
+ onError: (info) => console.error('[thread error]', info.error),
384
+ onTiming: (ms) => console.log(`[thread] task completed in ${ms.toFixed(1)}ms`),
385
+ ...(healthChecks ? { healthCheckInterval } : {}),
386
+ ...(onMetrics ? { onMetrics } : {}),
387
+ ...(onLog ? { onLog } : {}),
388
+ };
389
+
390
+ return createThread(definition, threadOpts);
391
+ }
392
+
393
+ // ---------------------------------------------------------------------------
394
+ // createGPUOp
395
+ // ---------------------------------------------------------------------------
396
+
397
+ /**
398
+ * Create a {@link GPUCompute} instance with a single custom operation
399
+ * already registered.
400
+ *
401
+ * This is the **quickest** way to get a GPU-accelerated operation running.
402
+ * Pass a plain JS function — it is auto-transpiled to WGSL for the GPU
403
+ * and also serves as the CPU fallback.
404
+ *
405
+ * @param {string} name
406
+ * Operation name (used in subsequent `gpu.run(name, ...)` calls).
407
+ * @param {Function} fn
408
+ * A JS function `(data, uniforms) => expression`.
409
+ * - `data.value` → current element (auto-maps to `input[i]`)
410
+ * - `data.index` / `data.i` → current index
411
+ * - Second param is an object of uniform values
412
+ * @param {import('./types.js').GPUComputeOptions} [options={}]
413
+ * Additional GPU options forwarded to the constructor.
414
+ * @returns {GPUCompute} A GPU instance ready to `run(name, ...)`.
415
+ *
416
+ * @example
417
+ * ```js
418
+ * import { createGPUOp } from './factory.js';
419
+ *
420
+ * // EMA (Exponential Moving Average) — single line
421
+ * const gpu = createGPUOp('ema', (data, { alpha }) =>
422
+ * data.value * alpha + data.index * (1 - alpha)
423
+ * );
424
+ *
425
+ * const prices = new Float32Array([100, 102, 101, 105, 107]);
426
+ * const result = await gpu.run('ema', {
427
+ * inputs: { data: prices },
428
+ * uniforms: { alpha: new Float32Array([0.3]) },
429
+ * outputs: { result: 5 },
430
+ * });
431
+ * // result.result → Float32Array of EMA values
432
+ * ```
433
+ *
434
+ * @example
435
+ * ```js
436
+ * // Clamp values between 0 and 1
437
+ * const gpu = createGPUOp('clamp01', (data) =>
438
+ * Math.min(Math.max(data.value, 0), 1)
439
+ * );
440
+ *
441
+ * const noisy = new Float32Array([-0.5, 0.3, 1.2, 0.8]);
442
+ * const result = await gpu.run('clamp01', {
443
+ * inputs: { data: noisy },
444
+ * outputs: { result: 4 },
445
+ * });
446
+ * // result.result → [0, 0.3, 1, 0.8]
447
+ * ```
448
+ *
449
+ * @example
450
+ * ```js
451
+ * // Z-score normalization
452
+ * const gpu = createGPUOp('zscore', (data, { mean, std }) =>
453
+ * (data.value - mean) / std
454
+ * );
455
+ *
456
+ * const raw = new Float32Array([10, 20, 30, 40, 50]);
457
+ * const result = await gpu.run('zscore', {
458
+ * inputs: { data: raw },
459
+ * uniforms: { mean: new Float32Array([30]), std: new Float32Array([14.14]) },
460
+ * outputs: { result: 5 },
461
+ * });
462
+ * ```
463
+ */
464
+ export function createGPUOp(name, fn, options = {}) {
465
+ const gpu = new GPUCompute(options);
466
+ gpu.define(name, fn);
467
+ return gpu;
468
+ }
469
+
470
+ // ---------------------------------------------------------------------------
471
+ // createGPUPipeline
472
+ // ---------------------------------------------------------------------------
473
+
474
+ /**
475
+ * Create a {@link GPUCompute} instance with multiple operations
476
+ * pre-registered from an array of `[name, declaration]` pairs.
477
+ *
478
+ * Ideal for building a reusable compute pipeline where several
479
+ * operations are defined upfront and used interchangeably.
480
+ *
481
+ * @param {Array<[string, Function|import('./types.js').OpDeclaration]>} ops
482
+ * Array of `[name, declaration]` tuples.
483
+ * Each declaration can be a JS function or an `{ inputs, body, fn, ... }`
484
+ * object.
485
+ * @param {import('./types.js').GPUComputeOptions} [options={}]
486
+ * Additional GPU options forwarded to the constructor.
487
+ * @returns {GPUCompute} A GPU instance with all ops registered.
488
+ *
489
+ * @example
490
+ * ```js
491
+ * import { createGPUPipeline } from './factory.js';
492
+ *
493
+ * const gpu = createGPUPipeline([
494
+ * ['ema', (data, { alpha }) => data.value * alpha],
495
+ * ['double', (data) => data.value * 2],
496
+ * ['negate', (data) => -data.value],
497
+ * ['sqrt', (data) => Math.sqrt(Math.abs(data.value))],
498
+ * ]);
499
+ *
500
+ * // Run any of them
501
+ * await gpu.run('ema', { inputs: { data }, uniforms: { alpha: new Float32Array([0.5]) }, outputs: { result: 5 } });
502
+ * await gpu.run('double', { inputs: { data }, outputs: { result: 5 } });
503
+ *
504
+ * console.log(gpu.ops); // ['ema', 'double', 'negate', 'sqrt']
505
+ * ```
506
+ *
507
+ * @example
508
+ * ```js
509
+ * // Mix JS functions and WGSL body declarations
510
+ * const gpu = createGPUPipeline([
511
+ * ['scale', (data, { factor }) => data.value * factor],
512
+ * ['clamp', {
513
+ * inputs: ['data'],
514
+ * outputs: ['result'],
515
+ * uniforms: ['minVal', 'maxVal'],
516
+ * body: 'result[i] = clamp(data[i], minVal, maxVal);',
517
+ * }],
518
+ * ]);
519
+ * ```
520
+ */
521
+ export function createGPUPipeline(ops, options = {}) {
522
+ const gpu = new GPUCompute(options);
523
+ for (const [name, decl] of ops) {
524
+ gpu.define(name, decl);
525
+ }
526
+ return gpu;
527
+ }
528
+
529
+ // ---------------------------------------------------------------------------
530
+ // createGPUReducer
531
+ // ---------------------------------------------------------------------------
532
+
533
+ /**
534
+ * Create a {@link GPUCompute} instance ready for reduction operations.
535
+ *
536
+ * The `reduce_sum`, `reduce_min`, and `reduce_max` ops are already
537
+ * built-in to {@link GPUCompute} as special multi-pass ops — this
538
+ * factory simply returns an instance without requiring a shader,
539
+ * so you can call `run('reduce_sum', …)` immediately.
540
+ *
541
+ * @param {import('./types.js').GPUComputeOptions} [options={}]
542
+ * Additional GPU options forwarded to the constructor.
543
+ * @returns {GPUCompute} A GPU instance ready for reductions.
544
+ *
545
+ * @example
546
+ * ```js
547
+ * import { createGPUReducer } from './factory.js';
548
+ *
549
+ * const gpu = createGPUReducer();
550
+ *
551
+ * const data = new Float32Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
552
+ *
553
+ * const sum = await gpu.run('reduce_sum', {
554
+ * inputs: { data },
555
+ * outputs: { result: 10 },
556
+ * });
557
+ * console.log(sum.result); // Float32Array([55])
558
+ *
559
+ * const max = await gpu.run('reduce_max', {
560
+ * inputs: { data },
561
+ * outputs: { result: 10 },
562
+ * });
563
+ * console.log(max.result); // Float32Array([10])
564
+ * ```
565
+ *
566
+ * @example
567
+ * ```js
568
+ * // Combine with other ops for full data processing
569
+ * const gpu = createGPUReducer();
570
+ * gpu.define('normalize', (data, { mean, std }) =>
571
+ * (data.value - mean) / std
572
+ * );
573
+ *
574
+ * const raw = new Float32Array([10, 20, 30, 40, 50]);
575
+ *
576
+ * // Normalize first, then find the sum
577
+ * const norm = await gpu.run('normalize', {
578
+ * inputs: { data: raw },
579
+ * uniforms: { mean: new Float32Array([30]), std: new Float32Array([14.14]) },
580
+ * outputs: { result: 5 },
581
+ * });
582
+ *
583
+ * const total = await gpu.run('reduce_sum', {
584
+ * inputs: { data: norm.result },
585
+ * outputs: { result: 5 },
586
+ * });
587
+ * ```
588
+ */
589
+ export function createGPUReducer(options = {}) {
590
+ return new GPUCompute(options);
591
+ }