@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,227 @@
1
+ /**
2
+ * @file Framework adapters for GPU compute.
3
+ *
4
+ * These adapters bridge {@link GPUCompute} to Zustand, Preact Signals,
5
+ * and any generic reactive setter. Unlike the thread adapters which
6
+ * rely on an event emitter, GPU adapters intercept `run()` calls
7
+ * directly because {@link GPUCompute} does not emit events.
8
+ *
9
+ * @module gpu/adapters
10
+ */
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // Zustand adapter
14
+ // ---------------------------------------------------------------------------
15
+
16
+ /**
17
+ * Bind a GPU compute instance's results to a **Zustand** store.
18
+ *
19
+ * Every time `binder.run()` resolves, the specified store action is
20
+ * called with the result. Errors can optionally be routed to a
21
+ * separate action.
22
+ *
23
+ * @param {import('./gpu.js').GPUCompute} gpu
24
+ * The GPU instance to bind.
25
+ * @param {Function} store
26
+ * The Zustand hook (e.g. `useStore`).
27
+ * @param {string} action
28
+ * Name of the store action to call with results.
29
+ * @param {Object} [options={}]
30
+ * @param {string} [options.errorAction]
31
+ * Store action to call with error messages on failure.
32
+ * @param {Function} [options.transform]
33
+ * Transform the result before writing to the store.
34
+ * @param {string} [options.metricsAction]
35
+ * Store action to call with metrics snapshots.
36
+ * @returns {{ run: Function, destroy: Function }}
37
+ *
38
+ * @example
39
+ * ```js
40
+ * import { create } from 'zustand';
41
+ * import { createGPUCompute } from './gpu.js';
42
+ * import { createGPUBinder } from './adapters.js';
43
+ *
44
+ * const useStore = create((set) => ({
45
+ * result: null,
46
+ * loading: false,
47
+ * error: null,
48
+ * setData: (data) => set({ result: data, loading: false }),
49
+ * setError: (err) => set({ error: err, loading: false }),
50
+ * setLoading: () => set({ loading: true, error: null }),
51
+ * }));
52
+ *
53
+ * const gpu = createGPUCompute({ shader: myShader });
54
+ * const binder = createGPUBinder(gpu, useStore, 'setData', {
55
+ * errorAction: 'setError',
56
+ * });
57
+ *
58
+ * await binder.run('add', data1, data2);
59
+ * const data = useStore.getState().result;
60
+ * ```
61
+ */
62
+ export function createGPUBinder(gpu, store, action, options = {}) {
63
+ const { errorAction = null, transform = null, metricsAction = null } = options;
64
+
65
+ const originalRun = gpu.run.bind(gpu);
66
+
67
+ const wrappedRun = async (...args) => {
68
+ const state = store.getState();
69
+ if (state[action]) state[action].loading?.call ? undefined : undefined;
70
+ if (state.setLoading) state.setLoading();
71
+
72
+ try {
73
+ const result = await originalRun(...args);
74
+ const finalResult = transform ? transform(result) : result;
75
+ store.getState()[action](finalResult);
76
+ if (metricsAction && store.getState()[metricsAction]) {
77
+ store.getState()[metricsAction](gpu.metrics);
78
+ }
79
+ return result;
80
+ } catch (err) {
81
+ if (errorAction && store.getState()[errorAction]) {
82
+ store.getState()[errorAction](err.message || String(err));
83
+ }
84
+ throw err;
85
+ }
86
+ };
87
+
88
+ return {
89
+ run: wrappedRun,
90
+ destroy: () => {
91
+ // wrappedRun holds originalRun in closure — nothing to undo
92
+ },
93
+ };
94
+ }
95
+
96
+ // ---------------------------------------------------------------------------
97
+ // Preact Signals adapter
98
+ // ---------------------------------------------------------------------------
99
+
100
+ /**
101
+ * Bind a GPU compute instance's results to a **Preact Signal**.
102
+ *
103
+ * Every time `binder.run()` resolves, the signal's value is updated.
104
+ * Errors can optionally be written to a separate signal.
105
+ *
106
+ * @param {import('./gpu.js').GPUCompute} gpu
107
+ * The GPU instance to bind.
108
+ * @param {Object} signal
109
+ * A Preact Signal (or any object with `.value` getter/setter).
110
+ * @param {Object} [options={}]
111
+ * @param {Object} [options.errorSignal]
112
+ * A signal to write errors into.
113
+ * @param {Function} [options.transform]
114
+ * Transform the result before writing to the signal.
115
+ * @param {Object} [options.loadingSignal]
116
+ * A signal to write loading state (boolean) into.
117
+ * @returns {{ run: Function, destroy: Function }}
118
+ *
119
+ * @example
120
+ * ```js
121
+ * import { signal } from '@preact/signals';
122
+ * import { createGPUCompute } from './gpu.js';
123
+ * import { createGPUSignalBinder } from './adapters.js';
124
+ *
125
+ * const dataSignal = signal(null);
126
+ * const errorSignal = signal(null);
127
+ * const loadingSignal = signal(false);
128
+ *
129
+ * const gpu = createGPUCompute();
130
+ * const binder = createGPUSignalBinder(gpu, dataSignal, {
131
+ * errorSignal,
132
+ * loadingSignal,
133
+ * });
134
+ *
135
+ * await binder.run('ema', priceData, { alpha: 0.3 });
136
+ * console.log(dataSignal.value); // Float32Array of EMA values
137
+ * ```
138
+ */
139
+ export function createGPUSignalBinder(gpu, signal, options = {}) {
140
+ const { errorSignal = null, transform = null, loadingSignal = null } = options;
141
+
142
+ const originalRun = gpu.run.bind(gpu);
143
+
144
+ const wrappedRun = async (...args) => {
145
+ if (loadingSignal) loadingSignal.value = true;
146
+ if (errorSignal) errorSignal.value = null;
147
+
148
+ try {
149
+ const result = await originalRun(...args);
150
+ signal.value = transform ? transform(result) : result;
151
+ if (errorSignal) errorSignal.value = null;
152
+ if (loadingSignal) loadingSignal.value = false;
153
+ return result;
154
+ } catch (err) {
155
+ if (errorSignal) errorSignal.value = err.message || String(err);
156
+ if (loadingSignal) loadingSignal.value = false;
157
+ throw err;
158
+ }
159
+ };
160
+
161
+ return {
162
+ run: wrappedRun,
163
+ destroy: () => {},
164
+ };
165
+ }
166
+
167
+ // ---------------------------------------------------------------------------
168
+ // Generic reactive adapter
169
+ // ---------------------------------------------------------------------------
170
+
171
+ /**
172
+ * Bind a GPU compute instance's results to **any** reactive state
173
+ * system via a setter callback.
174
+ *
175
+ * Works with MobX, Redux, Vue refs, Svelte stores, or plain `setState`
176
+ * functions.
177
+ *
178
+ * @param {import('./gpu.js').GPUCompute} gpu
179
+ * The GPU instance to bind.
180
+ * @param {Function} setter
181
+ * Called with `(result)` on success.
182
+ * @param {Object} [options={}]
183
+ * @param {Function} [options.onError]
184
+ * Called with `(error)` on failure.
185
+ * @param {Function} [options.transform]
186
+ * Transform the result before passing to the setter.
187
+ * @param {Function} [options.onMetrics]
188
+ * Called with metrics snapshots after each run.
189
+ * @returns {{ run: Function, destroy: Function }}
190
+ *
191
+ * @example
192
+ * ```js
193
+ * import { createGPUStoreBinder } from './adapters.js';
194
+ *
195
+ * const gpu = createGPUCompute();
196
+ * const [data, setData] = useState(null);
197
+ *
198
+ * const binder = createGPUStoreBinder(gpu, setData, {
199
+ * onError: (err) => console.error(err),
200
+ * });
201
+ *
202
+ * await binder.run('ema', priceData, { alpha: 0.3 });
203
+ * // data is now set to the result
204
+ * ```
205
+ */
206
+ export function createGPUStoreBinder(gpu, setter, options = {}) {
207
+ const { onError = null, transform = null, onMetrics = null } = options;
208
+
209
+ const originalRun = gpu.run.bind(gpu);
210
+
211
+ const wrappedRun = async (...args) => {
212
+ try {
213
+ const result = await originalRun(...args);
214
+ setter(transform ? transform(result) : result);
215
+ if (onMetrics) onMetrics(gpu.metrics);
216
+ return result;
217
+ } catch (err) {
218
+ if (onError) onError(err.message || String(err));
219
+ throw err;
220
+ }
221
+ };
222
+
223
+ return {
224
+ run: wrappedRun,
225
+ destroy: () => {},
226
+ };
227
+ }
@@ -0,0 +1,261 @@
1
+ /**
2
+ * @file Pipeline chaining classes for GPU compute.
3
+ *
4
+ * Provides {@link PipelineChain} and {@link DataPipelineChain} for
5
+ * fluent, sequential GPU operation composition.
6
+ *
7
+ * @module gpu/chains
8
+ */
9
+
10
+ import { GPUComputeError } from '../error.js';
11
+ import { boxAll, isOldRunFormat, jsToWgsl, getParamNames } from './helpers.js';
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // PipelineChain — fluent API for chaining GPU operations
15
+ // ---------------------------------------------------------------------------
16
+
17
+ /**
18
+ * Fluent pipeline chain for sequencing GPU operations.
19
+ *
20
+ * Created by {@link GPUCompute.pipe}. Each method queues an operation,
21
+ * and `.result()` executes them all sequentially, feeding outputs
22
+ * as inputs to the next step.
23
+ *
24
+ * @example
25
+ * ```js
26
+ * const chain = gpu.pipe()
27
+ * .multiply({ inputs: { a: data }, uniforms: { b: new Float32Array([2.0]) } })
28
+ * .add({ uniforms: { b: new Float32Array([1.0]) } })
29
+ * .sqrt();
30
+ *
31
+ * const output = await chain.result();
32
+ * // output.result → Float32Array
33
+ * ```
34
+ */
35
+ export class PipelineChain {
36
+ /**
37
+ * Create a new pipeline chain.
38
+ *
39
+ * @param {import('./gpu.js').GPUCompute} gpu - GPUCompute instance.
40
+ */
41
+ constructor(gpu) {
42
+ /** @type {import('./gpu.js').GPUCompute} */
43
+ this._gpu = gpu;
44
+
45
+ /** @type {Array<{ name: string, input: Object }>} */
46
+ this._steps = [];
47
+ }
48
+
49
+ /**
50
+ * Add an operation to the chain.
51
+ *
52
+ * Accepts any of these forms:
53
+ * - `add('op', { inputs: {...}, uniforms: {...} })` — old format
54
+ * - `add('op', { a: data }, { b: scalar })` — new flat format
55
+ * - `add(x => Math.sqrt(x))` — JS function (auto-named, single input)
56
+ *
57
+ * @param {string|Function} nameOrFn - Operation name or JS function.
58
+ * @param {Object} [inputsOrSpec={}] - Input data (flat) or old-format spec.
59
+ * @param {Object} [uniforms={}] - Uniform values (flat).
60
+ * @returns {PipelineChain} This chain (for chaining).
61
+ */
62
+ add(nameOrFn, inputsOrSpec = {}, uniforms = {}) {
63
+ // JS function shorthand: add(x => sqrt(x))
64
+ if (typeof nameOrFn === 'function') {
65
+ const fn = nameOrFn;
66
+ const inputNames = getParamNames(fn);
67
+ const wgslBody = jsToWgsl(fn);
68
+ const opName = `_pipe_${wgslBody.replace(/\W+/g, '_').slice(0, 32)}`;
69
+
70
+ // Build CPU fallback
71
+ const cpuFn = async (input) => {
72
+ const src = input.inputs[inputNames[0]];
73
+ const result = new Float32Array(src.length);
74
+ for (let i = 0; i < src.length; i++) result[i] = fn(src[i]);
75
+ return { result };
76
+ };
77
+
78
+ this._gpu.define(opName, {
79
+ inputs: inputNames,
80
+ outputs: ['result'],
81
+ body: `result[i] = ${wgslBody};`,
82
+ fn: cpuFn,
83
+ });
84
+
85
+ this._steps.push({ name: opName, input: {} });
86
+ return this;
87
+ }
88
+
89
+ // Named op
90
+ let input;
91
+ if (isOldRunFormat(inputsOrSpec)) {
92
+ input = inputsOrSpec;
93
+ } else {
94
+ input = { inputs: boxAll(inputsOrSpec), uniforms: boxAll(uniforms) };
95
+ }
96
+ this._steps.push({ name: nameOrFn, input });
97
+ return this;
98
+ }
99
+
100
+ /**
101
+ * Execute all queued operations sequentially.
102
+ *
103
+ * Each step's output is merged into the next step's inputs.
104
+ * The final step's output is returned.
105
+ *
106
+ * @param {Object} [opts={}]
107
+ * @param {AbortSignal} [opts.signal] - Cancellation signal.
108
+ * @returns {Promise<Object<string, TypedArray>>}
109
+ */
110
+ async result(opts = {}) {
111
+ if (this._steps.length === 0) {
112
+ throw new GPUComputeError('Pipeline chain is empty — add steps first');
113
+ }
114
+
115
+ // Seed initial inputs from opts (e.g. result({ inputs: { data } }))
116
+ let carriedOutputs = {};
117
+ if (opts.inputs) {
118
+ carriedOutputs = boxAll(opts.inputs);
119
+ }
120
+
121
+ for (let i = 0; i < this._steps.length; i++) {
122
+ const { name, input } = this._steps[i];
123
+
124
+ if (opts.signal?.aborted) {
125
+ throw new GPUComputeError('Pipeline chain cancelled', opts.signal.reason ?? new DOMException('Aborted', 'AbortError'));
126
+ }
127
+
128
+ // Look up op definition to know its expected input names
129
+ const opDef = this._gpu._ops?.get(name);
130
+ const expectedInputs = opDef?.inputs || [];
131
+
132
+ // Remap carried outputs to match the op's expected input names
133
+ // If op expects `x` but carried has `result`, map result → x
134
+ let remappedCarried = { ...carriedOutputs };
135
+ if (expectedInputs.length === 1) {
136
+ const expectedName = expectedInputs[0];
137
+ if (!(expectedName in remappedCarried) && Object.keys(remappedCarried).length === 1) {
138
+ const [onlyKey] = Object.keys(remappedCarried);
139
+ if (onlyKey !== expectedName) {
140
+ remappedCarried = { [expectedName]: remappedCarried[onlyKey] };
141
+ }
142
+ }
143
+ }
144
+
145
+ // Merge: explicit step inputs > remapped carried outputs
146
+ const mergedInput = {
147
+ ...input,
148
+ inputs: { ...remappedCarried, ...(input.inputs || {}) },
149
+ signal: opts.signal,
150
+ };
151
+
152
+ // If user provided outputs in opts, only pass them for the first step
153
+ if (i === 0 && opts.outputs) {
154
+ mergedInput.outputs = opts.outputs;
155
+ }
156
+
157
+ const result = await this._gpu.run(name, mergedInput);
158
+ carriedOutputs = result;
159
+ }
160
+
161
+ return carriedOutputs;
162
+ }
163
+
164
+ /**
165
+ * Number of steps in the chain.
166
+ *
167
+ * @type {number}
168
+ */
169
+ get length() {
170
+ return this._steps.length;
171
+ }
172
+ }
173
+
174
+ // ---------------------------------------------------------------------------
175
+ // DataPipelineChain — pipe(data, count) with Proxy-based named methods
176
+ // ---------------------------------------------------------------------------
177
+
178
+ /**
179
+ * A pipeline chain that carries initial data and supports named methods.
180
+ *
181
+ * Created by `gpu.pipe(data, count)`. Predefined ops become methods
182
+ * via a Proxy, and `.result()` executes the chain.
183
+ *
184
+ * @example
185
+ * ```js
186
+ * const output = await gpu.pipe(new Float32Array([1, 2, 3]), 3)
187
+ * .ema({ alpha: 0.3 })
188
+ * .double()
189
+ * .result();
190
+ * // output.result → Float32Array
191
+ * ```
192
+ */
193
+ export class DataPipelineChain extends PipelineChain {
194
+ /**
195
+ * @param {import('./gpu.js').GPUCompute} gpu
196
+ * @param {TypedArray} data - Initial input data.
197
+ * @param {number} count - Element count.
198
+ */
199
+ constructor(gpu, data, count) {
200
+ super(gpu);
201
+ /** @type {TypedArray} */
202
+ this._data = data;
203
+ /** @type {number} */
204
+ this._count = count;
205
+ }
206
+
207
+ /**
208
+ * Execute all queued operations with the carried data.
209
+ *
210
+ * The first step receives `this._data` as its first input.
211
+ * Subsequent steps receive the previous step's output.
212
+ *
213
+ * @param {Object} [opts={}]
214
+ * @param {AbortSignal} [opts.signal] - Cancellation signal.
215
+ * @returns {Promise<Object<string, TypedArray>>}
216
+ */
217
+ async result(opts = {}) {
218
+ if (this._steps.length === 0) {
219
+ throw new GPUComputeError('Pipeline chain is empty — add steps first');
220
+ }
221
+
222
+ // Seed with the carried data, mapped to the first step's expected input name
223
+ const firstOpDef = this._gpu._ops?.get(this._steps[0].name);
224
+ const firstName = firstOpDef?.inputs?.[0] || 'data';
225
+ let carriedOutputs = { [firstName]: this._data };
226
+
227
+ for (let i = 0; i < this._steps.length; i++) {
228
+ const { name, input } = this._steps[i];
229
+
230
+ if (opts.signal?.aborted) {
231
+ throw new GPUComputeError('Pipeline chain cancelled', opts.signal.reason ?? new DOMException('Aborted', 'AbortError'));
232
+ }
233
+
234
+ const opDef = this._gpu._ops?.get(name);
235
+ const expectedInputs = opDef?.inputs || [];
236
+
237
+ // Remap carried outputs to match the op's expected input names
238
+ let remappedCarried = { ...carriedOutputs };
239
+ if (expectedInputs.length === 1) {
240
+ const expectedName = expectedInputs[0];
241
+ if (!(expectedName in remappedCarried) && Object.keys(remappedCarried).length === 1) {
242
+ const [onlyKey] = Object.keys(remappedCarried);
243
+ if (onlyKey !== expectedName) {
244
+ remappedCarried = { [expectedName]: remappedCarried[onlyKey] };
245
+ }
246
+ }
247
+ }
248
+
249
+ const mergedInput = {
250
+ ...input,
251
+ inputs: { ...remappedCarried, ...(input.inputs || {}) },
252
+ signal: opts.signal,
253
+ };
254
+
255
+ const result = await this._gpu.run(name, mergedInput);
256
+ carriedOutputs = result;
257
+ }
258
+
259
+ return carriedOutputs;
260
+ }
261
+ }