@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/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
|
+
}
|