@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/package.json ADDED
@@ -0,0 +1,133 @@
1
+ {
2
+ "name": "@nadrif/thread",
3
+ "version": "0.1.0",
4
+ "author": "Nadrif LLC",
5
+ "main": "./index.js",
6
+ "module": "./index.js",
7
+ "peerDependencies": {
8
+ "preact": ">=10.0.0",
9
+ "react": ">=18.0.0"
10
+ },
11
+ "exports": {
12
+ ".": {
13
+ "browser": {
14
+ "import": "./src/index.js",
15
+ "types": "./src/types.js"
16
+ },
17
+ "node": {
18
+ "import": "./src/node.js",
19
+ "types": "./src/types.js"
20
+ },
21
+ "bun": {
22
+ "import": "./src/node.js",
23
+ "types": "./src/types.js"
24
+ },
25
+ "deno": {
26
+ "import": "./src/deno.js",
27
+ "types": "./src/types.js"
28
+ },
29
+ "edge": {
30
+ "import": "./src/edge.js",
31
+ "types": "./src/types.js"
32
+ },
33
+ "import": "./src/index.js",
34
+ "types": "./src/types.js"
35
+ },
36
+ "./node": {
37
+ "import": "./src/node.js",
38
+ "types": "./src/types.js"
39
+ },
40
+ "./deno": {
41
+ "import": "./src/deno.js",
42
+ "types": "./src/types.js"
43
+ },
44
+ "./edge": {
45
+ "import": "./src/edge.js",
46
+ "types": "./src/types.js"
47
+ },
48
+ "./config": {
49
+ "import": "./src/config.js",
50
+ "types": "./src/types.js"
51
+ },
52
+ "./env": {
53
+ "import": "./src/env.js",
54
+ "types": "./src/types.js"
55
+ },
56
+ "./gpu": {
57
+ "import": "./src/gpu/index.js",
58
+ "types": "./src/types.js"
59
+ },
60
+ "./gpu/env": {
61
+ "import": "./src/gpu/env.js",
62
+ "types": "./src/types.js"
63
+ },
64
+ "./gpu/core": "./src/gpu/gpu.js",
65
+ "./gpu/shaders": "./src/gpu/shaders.js",
66
+ "./gpu/helpers": "./src/gpu/helpers.js",
67
+ "./gpu/chains": "./src/gpu/chains.js",
68
+ "./gpu/special": "./src/gpu/special.js",
69
+ "./gpu/hooks": "./src/gpu/hooks.js",
70
+ "./gpu/adapters": "./src/gpu/adapters.js",
71
+ "./thread": "./src/thread.js",
72
+ "./pool": "./src/pool.js",
73
+ "./hooks": "./src/hooks.js",
74
+ "./adapters": "./src/adapters.js",
75
+ "./factory": "./src/factory.js",
76
+ "./error": "./src/error.js",
77
+ "./metrix": "./src/metrix.js",
78
+ "./serializer": "./src/serializer.js",
79
+ "./workers": "./src/worker-factory.js",
80
+ "./types": "./src/types.js"
81
+ },
82
+ "description": "Enterprise Web Worker & GPU Compute framework — cross-platform thread pools, work-stealing, WebGPU shaders, with framework adapters for Preact/React/Svelte/Vue/Solid and state managers Zustand/Redux/Jotai/MobX/Signals.",
83
+ "engines": {
84
+ "node": ">=18.0.0",
85
+ "bun": ">=1.0.0",
86
+ "deno": ">=1.25.0"
87
+ },
88
+ "files": [
89
+ "src/"
90
+ ],
91
+ "keywords": [
92
+ "web-worker",
93
+ "thread-pool",
94
+ "webgpu",
95
+ "compute-shader",
96
+ "gpu",
97
+ "parallel",
98
+ "work-stealing",
99
+ "preact",
100
+ "react",
101
+ "svelte",
102
+ "vue",
103
+ "solid",
104
+ "zustand",
105
+ "redux",
106
+ "jotai",
107
+ "mobx",
108
+ "signals",
109
+ "deno",
110
+ "bun",
111
+ "node",
112
+ "edge",
113
+ "cloudflare-workers"
114
+ ],
115
+ "license": "MIT",
116
+ "peerDependenciesMeta": {
117
+ "preact": {
118
+ "optional": true
119
+ },
120
+ "react": {
121
+ "optional": true
122
+ }
123
+ },
124
+ "trustedDependencies": [
125
+ "webgpu"
126
+ ],
127
+ "type": "module",
128
+ "types": "./src/types.js",
129
+ "dependencies": {},
130
+ "optionalDependencies": {
131
+ "bun-webgpu": "^0.1.7"
132
+ }
133
+ }
@@ -0,0 +1,404 @@
1
+ /**
2
+ * @file Framework-agnostic adapters for connecting threads to state
3
+ * management libraries.
4
+ *
5
+ * These adapters let you pipe thread/pool results into **Zustand** stores,
6
+ * **Preact Signals**, or any custom state manager – without coupling
7
+ * the thread to a specific framework.
8
+ *
9
+ * Every adapter follows the same pattern:
10
+ *
11
+ * ```
12
+ * adapter(thread, ...) → { run, destroy }
13
+ * ```
14
+ *
15
+ * - `run(...args)` – execute the task **and** push the result to the store.
16
+ * - `destroy()` – clean up event listeners.
17
+ *
18
+ * **Why adapters instead of hooks?**
19
+ *
20
+ * Hooks are great inside components, but sometimes you need to wire a
21
+ * thread to a store **outside** the component tree (e.g. in a module,
22
+ * a web worker manager, or a service layer). Adapters work anywhere.
23
+ *
24
+ * @example
25
+ * ```js
26
+ * import { createZustandBinder } from './adapters.js';
27
+ * import { useDataStore } from '../stores/useDataStore.js';
28
+ * import { createThread } from './factory.js';
29
+ *
30
+ * const thread = createThread((query) => db.query(query));
31
+ *
32
+ * // Bind thread results to Zustand store
33
+ * const binder = createZustandBinder(thread, useDataStore, 'setData');
34
+ *
35
+ * // Now every thread.run() result is pushed to the store
36
+ * await binder.run('SELECT * FROM users');
37
+ * // → useDataStore.getState().data is now the result
38
+ * ```
39
+ *
40
+ * @module adapters
41
+ */
42
+
43
+ // ---------------------------------------------------------------------------
44
+ // Zustand adapter
45
+ // ---------------------------------------------------------------------------
46
+
47
+ /**
48
+ * Bind a thread's results to a **Zustand** store action.
49
+ *
50
+ * Every time `run()` resolves, the result is passed to the specified
51
+ * store action (typically a `set` call). Errors are also forwarded to
52
+ * an optional error action.
53
+ *
54
+ * @param {import('./thread.js').Thread} thread
55
+ * The thread to bind.
56
+ * @param {Function} store
57
+ * The Zustand store hook (the return value of `create()`).
58
+ * @param {string} action
59
+ * Name of the store action to call with the result. The action
60
+ * receives `(result)` as its argument.
61
+ * @param {Object} [options={}]
62
+ * @param {string} [options.errorAction]
63
+ * Name of the store action to call on error. Receives
64
+ * `(errorMessage)`. If omitted, errors are thrown (not swallowed).
65
+ * @param {boolean} [options.append=false]
66
+ * If `true`, the result is **appended** to an array in the store
67
+ * instead of replacing it. Useful for log-style stores.
68
+ * @param {string} [options.metricsAction]
69
+ * Name of the store action to call with every metrics snapshot.
70
+ * @returns {{ run: Function, destroy: Function }}
71
+ * `run(...args)` – execute the task and push to the store.
72
+ * `destroy()` – stop listening to thread events.
73
+ *
74
+ * @example
75
+ * ```js
76
+ * import { create } from 'zustand';
77
+ * import { createZustandBinder } from './adapters.js';
78
+ *
79
+ * const useStore = create((set) => ({
80
+ * data: null,
81
+ * loading: false,
82
+ * error: null,
83
+ * setData: (data) => set({ data, loading: false }),
84
+ * setError: (error) => set({ error, loading: false }),
85
+ * setLoading: () => set({ loading: true, error: null }),
86
+ * }));
87
+ *
88
+ * const thread = createThread((query) => db.query(query));
89
+ * const binder = createZustandBinder(thread, useStore, 'setData', {
90
+ * errorAction: 'setError',
91
+ * });
92
+ *
93
+ * // In a component:
94
+ * await binder.run('SELECT * FROM users');
95
+ * const data = useStore.getState().data;
96
+ * ```
97
+ *
98
+ * @example
99
+ * ```js
100
+ * // Append mode – for streaming logs
101
+ * const logThread = createWorker((msg) => ({ text: msg, ts: Date.now() }));
102
+ * const binder = createZustandBinder(logThread, useLogStore, 'appendLog', {
103
+ * append: true,
104
+ * });
105
+ *
106
+ * binder.run('User logged in');
107
+ * binder.run('Page loaded');
108
+ * // useLogStore.getState().logs === [{ text: '...', ts: ... }, ...]
109
+ * ```
110
+ */
111
+ export function createZustandBinder(thread, store, action, options = {}) {
112
+ const { errorAction = null, append = false, metricsAction = null } = options;
113
+
114
+ const onResult = (result) => {
115
+ const state = store.getState();
116
+ if (append) {
117
+ const current = state[action] || state[Object.keys(state).find((k) => Array.isArray(state[k]))];
118
+ // If append mode, the action should accept (item) and handle appending
119
+ store.getState()[action](result);
120
+ } else {
121
+ store.getState()[action](result);
122
+ }
123
+ };
124
+
125
+ const onError = (info) => {
126
+ if (errorAction && store.getState()[errorAction]) {
127
+ store.getState()[errorAction](info.error || String(info));
128
+ }
129
+ };
130
+
131
+ const onMetrics = metricsAction
132
+ ? (snap) => {
133
+ if (store.getState()[metricsAction]) {
134
+ store.getState()[metricsAction](snap);
135
+ }
136
+ }
137
+ : null;
138
+
139
+ thread.on('result', onResult);
140
+ thread.on('error', onError);
141
+ if (onMetrics) thread.on('metrics', onMetrics);
142
+
143
+ return {
144
+ run: (...args) => thread.run(...args),
145
+ destroy: () => {
146
+ thread.off('result', onResult);
147
+ thread.off('error', onError);
148
+ if (onMetrics) thread.off('metrics', onMetrics);
149
+ },
150
+ };
151
+ }
152
+
153
+ // ---------------------------------------------------------------------------
154
+ // Preact Signals adapter
155
+ // ---------------------------------------------------------------------------
156
+
157
+ /**
158
+ * Bind a thread's results to a **Preact Signal**.
159
+ *
160
+ * Every time `run()` resolves, the signal's value is updated. Errors
161
+ * can optionally be written to a separate signal.
162
+ *
163
+ * @param {import('./thread.js').Thread} thread
164
+ * The thread to bind.
165
+ * @param {Object} signal
166
+ * A Preact Signal (or any object with `.value` getter/setter).
167
+ * @param {Object} [options={}]
168
+ * @param {Object} [options.errorSignal]
169
+ * A signal to write errors into (the signal's `.value` is set to
170
+ * the error message string, or `null` on success).
171
+ * @param {Function} [options.transform]
172
+ * Optional transform applied to the result before writing to the
173
+ * signal: `(result) => transformedResult`.
174
+ * @returns {{ run: Function, destroy: Function }}
175
+ *
176
+ * @example
177
+ * ```js
178
+ * import { signal } from '@preact/signals';
179
+ * import { createSignalBinder } from './adapters.js';
180
+ *
181
+ * const dataSignal = signal(null);
182
+ * const errorSignal = signal(null);
183
+ *
184
+ * const thread = createThread((x) => x * 2);
185
+ * const binder = createSignalBinder(thread, dataSignal, {
186
+ * errorSignal,
187
+ * });
188
+ *
189
+ * await binder.run(21);
190
+ * console.log(dataSignal.value); // 42
191
+ * console.log(errorSignal.value); // null
192
+ * ```
193
+ *
194
+ * @example
195
+ * ```js
196
+ * // With transform
197
+ * const countSignal = signal(0);
198
+ * const binder = createSignalBinder(thread, countSignal, {
199
+ * transform: (result) => result.length,
200
+ * });
201
+ *
202
+ * await binder.run(['a', 'b', 'c']);
203
+ * console.log(countSignal.value); // 3
204
+ * ```
205
+ */
206
+ export function createSignalBinder(thread, signal, options = {}) {
207
+ const { errorSignal = null, transform = null } = options;
208
+
209
+ const onResult = (result) => {
210
+ signal.value = transform ? transform(result) : result;
211
+ if (errorSignal) errorSignal.value = null;
212
+ };
213
+
214
+ const onError = (info) => {
215
+ if (errorSignal) {
216
+ errorSignal.value = info.error || String(info);
217
+ }
218
+ };
219
+
220
+ thread.on('result', onResult);
221
+ thread.on('error', onError);
222
+
223
+ return {
224
+ run: (...args) => thread.run(...args),
225
+ destroy: () => {
226
+ thread.off('result', onResult);
227
+ thread.off('error', onError);
228
+ },
229
+ };
230
+ }
231
+
232
+ // ---------------------------------------------------------------------------
233
+ // Generic reactive adapter
234
+ // ---------------------------------------------------------------------------
235
+
236
+ /**
237
+ * Bind a thread's results to **any** reactive state system via a setter
238
+ * callback.
239
+ *
240
+ * This is the most generic adapter – it works with MobX, Redux, Vue
241
+ * refs, Svelte stores, or plain `setState` functions.
242
+ *
243
+ * @param {import('./thread.js').Thread} thread
244
+ * The thread to bind.
245
+ * @param {Function} setter
246
+ * Called with `(result)` on success. Typically a state setter like
247
+ * `setState` or `store.set`.
248
+ * @param {Object} [options={}]
249
+ * @param {Function} [options.onError]
250
+ * Called with `(error)` on failure.
251
+ * @param {Function} [options.transform]
252
+ * Transform the result before passing to the setter.
253
+ * @param {Function} [options.onMetrics]
254
+ * Called with every metrics snapshot.
255
+ * @returns {{ run: Function, destroy: Function }}
256
+ *
257
+ * @example
258
+ * ```js
259
+ * import { createStoreBinder } from './adapters.js';
260
+ *
261
+ * // With React/Preact setState
262
+ * function MyComponent() {
263
+ * const [data, setData] = useState(null);
264
+ * const thread = useMemo(() => createThread(process), []);
265
+ *
266
+ * useEffect(() => {
267
+ * const binder = createStoreBinder(thread, setData, {
268
+ * onError: (err) => console.error(err),
269
+ * });
270
+ * return binder.destroy;
271
+ * }, [thread]);
272
+ *
273
+ * return <div>{JSON.stringify(data)}</div>;
274
+ * }
275
+ * ```
276
+ *
277
+ * @example
278
+ * ```js
279
+ * // With a plain object
280
+ * const state = { items: [] };
281
+ * const binder = createStoreBinder(thread, (result) => {
282
+ * state.items = result;
283
+ * render(); // re-render
284
+ * });
285
+ * ```
286
+ */
287
+ export function createStoreBinder(thread, setter, options = {}) {
288
+ const { onError = null, transform = null, onMetrics = null } = options;
289
+
290
+ const onResult = (result) => {
291
+ setter(transform ? transform(result) : result);
292
+ };
293
+
294
+ const errorHandler = (info) => {
295
+ if (onError) onError(info.error || String(info));
296
+ };
297
+
298
+ thread.on('result', onResult);
299
+ thread.on('error', errorHandler);
300
+ if (onMetrics) thread.on('metrics', onMetrics);
301
+
302
+ return {
303
+ run: (...args) => thread.run(...args),
304
+ destroy: () => {
305
+ thread.off('result', onResult);
306
+ thread.off('error', errorHandler);
307
+ if (onMetrics) thread.off('metrics', onMetrics);
308
+ },
309
+ };
310
+ }
311
+
312
+ // ---------------------------------------------------------------------------
313
+ // Pool adapter
314
+ // ---------------------------------------------------------------------------
315
+
316
+ /**
317
+ * Bind a thread pool's results to a reactive state system.
318
+ *
319
+ * Works like {@link createStoreBinder} but for pools. Additionally
320
+ * supports batching – multiple pool results can be accumulated and
321
+ * flushed together.
322
+ *
323
+ * @param {import('./pool.js').ThreadPool} pool
324
+ * The thread pool to bind.
325
+ * @param {Function} setter
326
+ * Called with `(result)` on each task completion.
327
+ * @param {Object} [options={}]
328
+ * @param {Function} [options.onError]
329
+ * Called with `(error)` on failure.
330
+ * @param {Function} [options.onMetrics]
331
+ * Called with pool metrics updates.
332
+ * @param {number} [options.batchInterval]
333
+ * If set, results are accumulated for this many ms and then flushed
334
+ * as an array to the setter. Useful for high-throughput scenarios.
335
+ * @returns {{ run: Function, destroy: Function, flush: Function }}
336
+ * `flush()` – manually flush accumulated batch results.
337
+ *
338
+ * @example
339
+ * ```js
340
+ * import { createPoolBinder } from './adapters.js';
341
+ *
342
+ * const pool = createPool(4, heavyComputation);
343
+ * const results = [];
344
+ *
345
+ * const binder = createPoolBinder(pool, (result) => {
346
+ * results.push(result);
347
+ * renderDashboard(results);
348
+ * }, {
349
+ * batchInterval: 500, // flush every 500ms
350
+ * onMetrics: (snap) => updateMetrics(snap),
351
+ * });
352
+ *
353
+ * items.forEach((item) => binder.run(item));
354
+ * ```
355
+ */
356
+ export function createPoolBinder(pool, setter, options = {}) {
357
+ const { onError = null, onMetrics = null, batchInterval = null } = options;
358
+
359
+ let batch = [];
360
+ let batchTimer = null;
361
+
362
+ const flush = () => {
363
+ if (batch.length > 0) {
364
+ setter(batch);
365
+ batch = [];
366
+ }
367
+ if (batchTimer) {
368
+ clearInterval(batchTimer);
369
+ batchTimer = null;
370
+ }
371
+ };
372
+
373
+ const onTaskResult = (result) => {
374
+ if (batchInterval) {
375
+ batch.push(result);
376
+ } else {
377
+ setter(result);
378
+ }
379
+ };
380
+
381
+ const onTaskError = (info) => {
382
+ if (onError) onError(info.error || String(info));
383
+ };
384
+
385
+ pool.on?.('result', onTaskResult);
386
+ pool.on?.('error', onTaskError);
387
+ if (onMetrics) pool.on?.('metrics', onMetrics);
388
+
389
+ if (batchInterval) {
390
+ batchTimer = setInterval(flush, batchInterval);
391
+ }
392
+
393
+ return {
394
+ run: (...args) => pool.run(...args),
395
+ flush,
396
+ destroy: () => {
397
+ flush();
398
+ pool.off?.('result', onTaskResult);
399
+ pool.off?.('error', onTaskError);
400
+ if (onMetrics) pool.off?.('metrics', onMetrics);
401
+ if (batchTimer) clearInterval(batchTimer);
402
+ },
403
+ };
404
+ }