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