@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/hooks.js
ADDED
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Framework-agnostic hooks for the thread module.
|
|
3
|
+
*
|
|
4
|
+
* These hooks manage thread and pool lifecycles automatically – threads
|
|
5
|
+
* are created on mount and terminated on unmount. They also provide
|
|
6
|
+
* reactive state for metrics, loading, and errors.
|
|
7
|
+
*
|
|
8
|
+
* The framework (Preact, React, etc.) is resolved at load time via the
|
|
9
|
+
* thread config system. No hardcoded framework dependency.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```jsx
|
|
13
|
+
* import { useThread, usePool, useThreadMetrics } from './hooks.js';
|
|
14
|
+
*
|
|
15
|
+
* function DataProcessor({ data }) {
|
|
16
|
+
* const { run, loading, error, result } = useThread(
|
|
17
|
+
* (items, ctx) => {
|
|
18
|
+
* ctx.log('Processing...');
|
|
19
|
+
* return items.map((x) => x * 2);
|
|
20
|
+
* },
|
|
21
|
+
* { timeout: 10_000 }
|
|
22
|
+
* );
|
|
23
|
+
*
|
|
24
|
+
* const metrics = useThreadMetrics();
|
|
25
|
+
*
|
|
26
|
+
* return (
|
|
27
|
+
* <div>
|
|
28
|
+
* {loading && <Spinner />}
|
|
29
|
+
* {error && <Error message={error} />}
|
|
30
|
+
* {result && <Results data={result} />}
|
|
31
|
+
* <MetricsBar avg={metrics.avg} throughput={metrics.throughput} />
|
|
32
|
+
* </div>
|
|
33
|
+
* );
|
|
34
|
+
* }
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* @module hooks
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
import { getHooks } from './config/index.js';
|
|
41
|
+
import { Thread } from "./thread.js";
|
|
42
|
+
import { ThreadPool } from "./pool.js";
|
|
43
|
+
|
|
44
|
+
// Resolve framework hooks at module load time (top-level await)
|
|
45
|
+
let useState, useEffect, useRef, useCallback, useMemo;
|
|
46
|
+
try {
|
|
47
|
+
({ useState, useEffect, useRef, useCallback, useMemo } = await getHooks());
|
|
48
|
+
} catch {
|
|
49
|
+
const _stub = () => { throw new Error('[thread] No framework installed. Install preact or react to use hooks.'); };
|
|
50
|
+
useState = _stub; useEffect = _stub; useRef = _stub; useCallback = _stub; useMemo = _stub;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// useThread
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Create and manage a Web Worker thread bound to a component's lifecycle.
|
|
59
|
+
*
|
|
60
|
+
* The thread is created on first render and **automatically terminated**
|
|
61
|
+
* when the component unmounts. Returns a stable `run` function and
|
|
62
|
+
* reactive state for the latest result, error, and loading status.
|
|
63
|
+
*
|
|
64
|
+
* **Key behaviours:**
|
|
65
|
+
* - The thread instance is stored in a ref – it does not change between
|
|
66
|
+
* renders.
|
|
67
|
+
* - `run()` returns a promise **and** updates `result`/`error`/`loading`
|
|
68
|
+
* state for declarative rendering.
|
|
69
|
+
* - The thread is terminated on unmount (via `terminate()` by default, or
|
|
70
|
+
* `terminateGracefully()` if `graceful: true` is set).
|
|
71
|
+
* - The `definition` and `options` are captured once. To change them,
|
|
72
|
+
* pass a `key` to force re-creation.
|
|
73
|
+
*
|
|
74
|
+
* @param {import('./types.js').ThreadDefinition | Function} definition
|
|
75
|
+
* Worker definition (function or `{ setup, exec, cleanup }` object).
|
|
76
|
+
* @param {import('./types.js').ThreadOptions & { graceful?: boolean }} [options={}]
|
|
77
|
+
* Thread options plus `graceful: true` to use `terminateGracefully()`.
|
|
78
|
+
* @returns {{
|
|
79
|
+
* thread: Thread,
|
|
80
|
+
* run: (...args: any[]) => Promise<any>,
|
|
81
|
+
* runAsync: (...args: any[]) => void,
|
|
82
|
+
* result: any,
|
|
83
|
+
* error: string | null,
|
|
84
|
+
* loading: boolean,
|
|
85
|
+
* terminated: boolean,
|
|
86
|
+
* }}
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* ```jsx
|
|
90
|
+
* const { run, result, loading, error } = useThread((x) => x * 2);
|
|
91
|
+
*
|
|
92
|
+
* return (
|
|
93
|
+
* <button onClick={() => run(21)} disabled={loading}>
|
|
94
|
+
* {loading ? 'Computing...' : result ?? 'Click to compute'}
|
|
95
|
+
* </button>
|
|
96
|
+
* );
|
|
97
|
+
* ```
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```jsx
|
|
101
|
+
* // With stateful definition
|
|
102
|
+
* const { run } = useThread({
|
|
103
|
+
* setup() { return { count: 0 }; },
|
|
104
|
+
* exec(state, delta) {
|
|
105
|
+
* state.count += delta;
|
|
106
|
+
* return state.count;
|
|
107
|
+
* },
|
|
108
|
+
* }, { timeout: 5000 });
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
export function useThread(definition, options = {}) {
|
|
112
|
+
const { graceful = false, ...threadOpts } = options;
|
|
113
|
+
|
|
114
|
+
const threadRef = useRef(null);
|
|
115
|
+
const [result, setResult] = useState(null);
|
|
116
|
+
const [error, setError] = useState(null);
|
|
117
|
+
const [loading, setLoading] = useState(false);
|
|
118
|
+
const [terminated, setTerminated] = useState(false);
|
|
119
|
+
|
|
120
|
+
// Create thread once
|
|
121
|
+
if (threadRef.current === null) {
|
|
122
|
+
threadRef.current = new Thread(definition, threadOpts);
|
|
123
|
+
}
|
|
124
|
+
const thread = threadRef.current;
|
|
125
|
+
|
|
126
|
+
// Cleanup on unmount
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
return () => {
|
|
129
|
+
if (thread && !thread.terminated) {
|
|
130
|
+
if (graceful) {
|
|
131
|
+
thread.terminateGracefully();
|
|
132
|
+
} else {
|
|
133
|
+
thread.terminate();
|
|
134
|
+
}
|
|
135
|
+
setTerminated(true);
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
}, [thread, graceful]);
|
|
139
|
+
|
|
140
|
+
// Stable run function
|
|
141
|
+
const run = useCallback(
|
|
142
|
+
async (...args) => {
|
|
143
|
+
if (thread.terminated) {
|
|
144
|
+
const msg = "Thread is terminated";
|
|
145
|
+
setError(msg);
|
|
146
|
+
throw new Error(msg);
|
|
147
|
+
}
|
|
148
|
+
setLoading(true);
|
|
149
|
+
setError(null);
|
|
150
|
+
try {
|
|
151
|
+
const res = await thread.run(...args);
|
|
152
|
+
setResult(res);
|
|
153
|
+
setLoading(false);
|
|
154
|
+
return res;
|
|
155
|
+
} catch (err) {
|
|
156
|
+
const msg = err.message || String(err);
|
|
157
|
+
setError(msg);
|
|
158
|
+
setLoading(false);
|
|
159
|
+
throw err;
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
[thread],
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
// Fire-and-forget
|
|
166
|
+
const runAsync = useCallback(
|
|
167
|
+
(...args) => {
|
|
168
|
+
thread.runAsync(...args);
|
|
169
|
+
},
|
|
170
|
+
[thread],
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
return { thread, run, runAsync, result, error, loading, terminated };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
// usePool
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Create and manage a thread pool bound to a component's lifecycle.
|
|
182
|
+
*
|
|
183
|
+
* @param {number} size - Initial number of workers.
|
|
184
|
+
* @param {import('./types.js').ThreadDefinition | Function} definition
|
|
185
|
+
* Worker definition.
|
|
186
|
+
* @param {import('./types.js').PoolOptions & { graceful?: boolean }} [options={}]
|
|
187
|
+
* Pool options plus `graceful: true`.
|
|
188
|
+
* @returns {{
|
|
189
|
+
* pool: ThreadPool,
|
|
190
|
+
* run: (...args: any[]) => { id: number, promise: Promise<any> },
|
|
191
|
+
* cancel: (taskId: number) => boolean,
|
|
192
|
+
* status: () => import('./types.js').PoolStatus,
|
|
193
|
+
* metrics: import('./types.js').MetricsSnapshot,
|
|
194
|
+
* terminated: boolean,
|
|
195
|
+
* }}
|
|
196
|
+
*
|
|
197
|
+
* @example
|
|
198
|
+
* ```jsx
|
|
199
|
+
* function ParallelWorker({ items }) {
|
|
200
|
+
* const { run, status, metrics } = usePool(4, (item) => process(item));
|
|
201
|
+
*
|
|
202
|
+
* const handleRunAll = () => {
|
|
203
|
+
* items.forEach((item) => run(item));
|
|
204
|
+
* };
|
|
205
|
+
*
|
|
206
|
+
* return (
|
|
207
|
+
* <div>
|
|
208
|
+
* <button onClick={handleRunAll}>Process all</button>
|
|
209
|
+
* <p>{status().busy} threads busy</p>
|
|
210
|
+
* <p>Avg: {metrics.avg.toFixed(1)}ms</p>
|
|
211
|
+
* </div>
|
|
212
|
+
* );
|
|
213
|
+
* }
|
|
214
|
+
* ```
|
|
215
|
+
*/
|
|
216
|
+
export function usePool(size, definition, options = {}) {
|
|
217
|
+
const { graceful = false, ...poolOpts } = options;
|
|
218
|
+
|
|
219
|
+
const poolRef = useRef(null);
|
|
220
|
+
const [metricsSnapshot, setMetricsSnapshot] = useState({});
|
|
221
|
+
const [terminated, setTerminated] = useState(false);
|
|
222
|
+
|
|
223
|
+
// Create pool once
|
|
224
|
+
if (poolRef.current === null) {
|
|
225
|
+
poolRef.current = new ThreadPool(size, definition, poolOpts);
|
|
226
|
+
}
|
|
227
|
+
const pool = poolRef.current;
|
|
228
|
+
|
|
229
|
+
// Subscribe to metrics
|
|
230
|
+
useEffect(() => {
|
|
231
|
+
if (!pool) return;
|
|
232
|
+
const interval = setInterval(() => {
|
|
233
|
+
if (!pool.terminated) {
|
|
234
|
+
setMetricsSnapshot(pool.metrics);
|
|
235
|
+
}
|
|
236
|
+
}, 1000);
|
|
237
|
+
return () => clearInterval(interval);
|
|
238
|
+
}, [pool]);
|
|
239
|
+
|
|
240
|
+
// Cleanup on unmount
|
|
241
|
+
useEffect(() => {
|
|
242
|
+
return () => {
|
|
243
|
+
if (pool) {
|
|
244
|
+
if (graceful) {
|
|
245
|
+
pool.terminateGracefully();
|
|
246
|
+
} else {
|
|
247
|
+
pool.terminateAll();
|
|
248
|
+
}
|
|
249
|
+
setTerminated(true);
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
}, [pool, graceful]);
|
|
253
|
+
|
|
254
|
+
// Stable run
|
|
255
|
+
const run = useCallback(
|
|
256
|
+
(...args) => pool.run(...args),
|
|
257
|
+
[pool],
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
// Stable cancel
|
|
261
|
+
const cancel = useCallback(
|
|
262
|
+
(taskId) => pool.cancel(taskId),
|
|
263
|
+
[pool],
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
// Stable status
|
|
267
|
+
const status = useCallback(
|
|
268
|
+
() => pool.status(),
|
|
269
|
+
[pool],
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
return { pool, run, cancel, status, metrics: metricsSnapshot, terminated };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// ---------------------------------------------------------------------------
|
|
276
|
+
// useThreadMetrics
|
|
277
|
+
// ---------------------------------------------------------------------------
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Subscribe to a thread's metrics and re-render on updates.
|
|
281
|
+
*
|
|
282
|
+
* Polls `thread.metrics` at the given interval and triggers a re-render
|
|
283
|
+
* when the snapshot changes.
|
|
284
|
+
*
|
|
285
|
+
* @param {Thread} [thread] - Thread instance. If `null`/`undefined`,
|
|
286
|
+
* returns a zeroed snapshot.
|
|
287
|
+
* @param {number} [intervalMs=1000] - Polling interval in ms.
|
|
288
|
+
* @returns {import('./types.js').MetricsSnapshot}
|
|
289
|
+
*
|
|
290
|
+
* @example
|
|
291
|
+
* ```jsx
|
|
292
|
+
* function MetricsDisplay({ thread }) {
|
|
293
|
+
* const metrics = useThreadMetrics(thread, 500);
|
|
294
|
+
*
|
|
295
|
+
* return (
|
|
296
|
+
* <div>
|
|
297
|
+
* <span>{metrics.count} tasks</span>
|
|
298
|
+
* <span>{metrics.avg.toFixed(1)}ms avg</span>
|
|
299
|
+
* <span>{(metrics.errorRate * 100).toFixed(1)}% errors</span>
|
|
300
|
+
* </div>
|
|
301
|
+
* );
|
|
302
|
+
* }
|
|
303
|
+
* ```
|
|
304
|
+
*/
|
|
305
|
+
export function useThreadMetrics(thread, intervalMs = 1000) {
|
|
306
|
+
const [snapshot, setSnapshot] = useState(
|
|
307
|
+
thread ? thread.metrics : { count: 0, errors: 0, avg: 0, min: 0, max: 0, throughput: 0, errorRate: 0 },
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
useEffect(() => {
|
|
311
|
+
if (!thread) return;
|
|
312
|
+
const interval = setInterval(() => {
|
|
313
|
+
if (!thread.terminated) {
|
|
314
|
+
setSnapshot(thread.metrics);
|
|
315
|
+
}
|
|
316
|
+
}, intervalMs);
|
|
317
|
+
return () => clearInterval(interval);
|
|
318
|
+
}, [thread, intervalMs]);
|
|
319
|
+
|
|
320
|
+
return snapshot;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// ---------------------------------------------------------------------------
|
|
324
|
+
// usePoolMetrics
|
|
325
|
+
// ---------------------------------------------------------------------------
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Subscribe to a pool's metrics and re-render on updates.
|
|
329
|
+
*
|
|
330
|
+
* @param {ThreadPool} [pool] - Pool instance.
|
|
331
|
+
* @param {number} [intervalMs=1000] - Polling interval in ms.
|
|
332
|
+
* @returns {import('./types.js').MetricsSnapshot}
|
|
333
|
+
*
|
|
334
|
+
* @example
|
|
335
|
+
* ```jsx
|
|
336
|
+
* function PoolDashboard({ pool }) {
|
|
337
|
+
* const metrics = usePoolMetrics(pool);
|
|
338
|
+
* const status = useMemo(() => pool.status(), [pool, metrics]);
|
|
339
|
+
*
|
|
340
|
+
* return (
|
|
341
|
+
* <div>
|
|
342
|
+
* <p>{status.busy}/{status.total} threads busy</p>
|
|
343
|
+
* <p>Avg: {metrics.avg?.toFixed(1)}ms</p>
|
|
344
|
+
* <p>Throughput: {metrics.throughput?.toFixed(0)} t/s</p>
|
|
345
|
+
* </div>
|
|
346
|
+
* );
|
|
347
|
+
* }
|
|
348
|
+
* ```
|
|
349
|
+
*/
|
|
350
|
+
export function usePoolMetrics(pool, intervalMs = 1000) {
|
|
351
|
+
const [snapshot, setSnapshot] = useState(
|
|
352
|
+
pool ? pool.metrics : { count: 0, errors: 0, avg: 0, min: 0, max: 0, throughput: 0, errorRate: 0 },
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
useEffect(() => {
|
|
356
|
+
if (!pool) return;
|
|
357
|
+
const interval = setInterval(() => {
|
|
358
|
+
setSnapshot(pool.metrics);
|
|
359
|
+
}, intervalMs);
|
|
360
|
+
return () => clearInterval(interval);
|
|
361
|
+
}, [pool, intervalMs]);
|
|
362
|
+
|
|
363
|
+
return snapshot;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// ---------------------------------------------------------------------------
|
|
367
|
+
// useThreadWorker
|
|
368
|
+
// ---------------------------------------------------------------------------
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Get a stable reference to a thread's `run` function without managing
|
|
372
|
+
* lifecycle (for use when you manage the thread yourself).
|
|
373
|
+
*
|
|
374
|
+
* Returns a memoised `run` callback that always uses the same thread.
|
|
375
|
+
* Useful when the thread is created outside the component (e.g. in a
|
|
376
|
+
* store or module-level variable).
|
|
377
|
+
*
|
|
378
|
+
* @param {Thread} thread - An existing thread instance.
|
|
379
|
+
* @returns {{
|
|
380
|
+
* run: (...args: any[]) => Promise<any>,
|
|
381
|
+
* runAsync: (...args: any[]) => void,
|
|
382
|
+
* }}
|
|
383
|
+
*
|
|
384
|
+
* @example
|
|
385
|
+
* ```js
|
|
386
|
+
* // Shared thread across components
|
|
387
|
+
* const sharedThread = createWorker((x) => x * 2);
|
|
388
|
+
*
|
|
389
|
+
* function ComponentA() {
|
|
390
|
+
* const { run } = useThreadWorker(sharedThread);
|
|
391
|
+
* return <button onClick={() => run(5)}>A</button>;
|
|
392
|
+
* }
|
|
393
|
+
*
|
|
394
|
+
* function ComponentB() {
|
|
395
|
+
* const { run } = useThreadWorker(sharedThread);
|
|
396
|
+
* return <button onClick={() => run(10)}>B</button>;
|
|
397
|
+
* }
|
|
398
|
+
* ```
|
|
399
|
+
*/
|
|
400
|
+
export function useThreadWorker(thread) {
|
|
401
|
+
const run = useCallback(
|
|
402
|
+
(...args) => thread.run(...args),
|
|
403
|
+
[thread],
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
const runAsync = useCallback(
|
|
407
|
+
(...args) => thread.runAsync(...args),
|
|
408
|
+
[thread],
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
return useMemo(() => ({ run, runAsync }), [run, runAsync]);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// ---------------------------------------------------------------------------
|
|
415
|
+
// useThreadEvent
|
|
416
|
+
// ---------------------------------------------------------------------------
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Subscribe to a thread event and clean up on unmount.
|
|
420
|
+
*
|
|
421
|
+
* @param {Thread} thread - Thread instance.
|
|
422
|
+
* @param {import('./types.js').ThreadEventName} event - Event name.
|
|
423
|
+
* @param {Function} handler - Event handler.
|
|
424
|
+
*
|
|
425
|
+
* @example
|
|
426
|
+
* ```jsx
|
|
427
|
+
* function LogViewer({ thread }) {
|
|
428
|
+
* const [logs, setLogs] = useState([]);
|
|
429
|
+
*
|
|
430
|
+
* useThreadEvent(thread, 'log', (msg) => {
|
|
431
|
+
* setLogs((prev) => [...prev, msg]);
|
|
432
|
+
* });
|
|
433
|
+
*
|
|
434
|
+
* return (
|
|
435
|
+
* <pre>{logs.join('\n')}</pre>
|
|
436
|
+
* );
|
|
437
|
+
* }
|
|
438
|
+
* ```
|
|
439
|
+
*/
|
|
440
|
+
export function useThreadEvent(thread, event, handler) {
|
|
441
|
+
useEffect(() => {
|
|
442
|
+
if (!thread || !handler) return;
|
|
443
|
+
thread.on(event, handler);
|
|
444
|
+
return () => {
|
|
445
|
+
thread.off(event, handler);
|
|
446
|
+
};
|
|
447
|
+
}, [thread, event, handler]);
|
|
448
|
+
}
|