Skip to content

Commit 15e09c7

Browse files
committed
feat: add progress reporting and custom message handling to useWebWorker 🐹
1 parent fc26b0c commit 15e09c7

File tree

7 files changed

+245
-60
lines changed

7 files changed

+245
-60
lines changed

Diff for: README.md

+51-6
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ A React hook for easy Web Worker integration with TypeScript support.
1515
- ⚡ Non-blocking UI operations
1616
- 📦 Zero dependencies
1717
- ⏱️ Built-in timeout handling
18-
- 🎯 Function-based worker creation
1918
- 🔍 Comprehensive error handling
20-
- 📝 No additional files needed - write your worker logic inline
19+
- 📊 Progress reporting and custom message passing
20+
- 🎯 Function-based worker creation - No additional files needed - write your worker logic inline
2121

2222
### Installation
2323

@@ -33,24 +33,46 @@ yarn add @atom-universe/use-web-worker
3333

3434
```tsx
3535
import useWebWorker from '@atom-universe/use-web-worker';
36+
import { useState } from 'react';
3637

3738
function Example() {
39+
const [progress, setProgress] = useState(0);
40+
3841
const [workerFn, workerStatus, workerTerminate] = useWebWorker(
39-
data => {
40-
// Your computation logic here
41-
return data.reverse();
42+
(data, workerContext) => {
43+
const total = data.length;
44+
const result = [];
45+
46+
for (let i = 0; i < total; i++) {
47+
result.push(data[i] * 2);
48+
// Report progress every 10%
49+
if (i % Math.floor(total / 10) === 0) {
50+
const percentComplete = Math.floor((i / total) * 100);
51+
workerContext.postMessage(['PROGRESS', { percent: percentComplete }]);
52+
}
53+
}
54+
55+
return result;
4256
},
4357
{
4458
timeout: 30000, // 30 seconds timeout
4559
onError: error => {
4660
console.error('Computation error:', error);
4761
},
62+
onMessage: message => {
63+
// Handle progress updates
64+
if (message.type === 'PROGRESS') {
65+
setProgress(message.data.percent);
66+
}
67+
},
4868
}
4969
);
5070

5171
const handleProcess = async () => {
5272
try {
53-
const result = await workerFn([1, 2, 3]);
73+
// Generate array with 1000 items
74+
const data = Array.from({ length: 1000 }, (_, i) => i);
75+
const result = await workerFn(data);
5476
console.log('Result:', result);
5577
} catch (error) {
5678
console.error('Failed to process:', error);
@@ -66,6 +88,28 @@ function Example() {
6688
{workerStatus === 'RUNNING' && (
6789
<button onClick={() => workerTerminate('PENDING')}>Cancel</button>
6890
)}
91+
92+
{workerStatus === 'RUNNING' && (
93+
<div>
94+
<p>Progress: {progress}%</p>
95+
<div
96+
style={{
97+
width: '100%',
98+
backgroundColor: '#e9ecef',
99+
borderRadius: '4px',
100+
height: '20px',
101+
}}
102+
>
103+
<div
104+
style={{
105+
width: `${progress}%`,
106+
backgroundColor: '#007bff',
107+
height: '100%',
108+
}}
109+
/>
110+
</div>
111+
</div>
112+
)}
69113
</div>
70114
);
71115
}
@@ -81,6 +125,7 @@ function useWebWorker<T extends (...args: any[]) => any>(
81125
dependencies?: string[]; // External dependencies
82126
localDependencies?: Function[]; // Local function dependencies
83127
onError?: (error: Error) => void; // Error callback
128+
onMessage?: (message: { type: string; data: any }) => void; // Custom message handler
84129
}
85130
): [
86131
(...args: Parameters<T>) => Promise<ReturnType<T>>, // Worker function

Diff for: README_CN.md

+51-10
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@
1515
- ⚡ 不阻塞 UI 操作
1616
- 📦 零依赖
1717
- ⏱️ 内置超时处理
18-
- 🎯 基于函数的 Worker 创建
1918
- 🔍 全面的错误处理
20-
- 📝 无需额外文件 - 直接在代码中编写 Worker 逻辑
19+
- 📊 进度报告和自定义消息传递
20+
- 🎯 基于函数的 Worker 创建 - 直接在代码中编写 Worker 逻辑
2121

2222
## 安装
2323

@@ -33,24 +33,46 @@ yarn add @atom-universe/use-web-worker
3333

3434
```tsx
3535
import useWebWorker from '@atom-universe/use-web-worker';
36+
import { useState } from 'react';
3637

3738
function Example() {
39+
const [progress, setProgress] = useState(0);
40+
3841
const [workerFn, workerStatus, workerTerminate] = useWebWorker(
39-
data => {
40-
// 你的计算逻辑
41-
return data.reverse();
42+
(data, workerContext) => {
43+
const total = data.length;
44+
const result = [];
45+
46+
for (let i = 0; i < total; i++) {
47+
result.push(data[i] * 2);
48+
// 每完成10%上报一次进度
49+
if (i % Math.floor(total / 10) === 0) {
50+
const percentComplete = Math.floor((i / total) * 100);
51+
workerContext.postMessage(['PROGRESS', { percent: percentComplete }]);
52+
}
53+
}
54+
55+
return result;
4256
},
4357
{
4458
timeout: 30000, // 30 秒超时
4559
onError: error => {
4660
console.error('计算错误:', error);
4761
},
62+
onMessage: message => {
63+
// 处理进度更新
64+
if (message.type === 'PROGRESS') {
65+
setProgress(message.data.percent);
66+
}
67+
},
4868
}
4969
);
5070

5171
const handleProcess = async () => {
5272
try {
53-
const result = await workerFn([1, 2, 3]);
73+
// 生成包含1000个元素的数组
74+
const data = Array.from({ length: 1000 }, (_, i) => i);
75+
const result = await workerFn(data);
5476
console.log('结果:', result);
5577
} catch (error) {
5678
console.error('处理失败:', error);
@@ -66,6 +88,28 @@ function Example() {
6688
{workerStatus === 'RUNNING' && (
6789
<button onClick={() => workerTerminate('PENDING')}>取消</button>
6890
)}
91+
92+
{workerStatus === 'RUNNING' && (
93+
<div>
94+
<p>进度: {progress}%</p>
95+
<div
96+
style={{
97+
width: '100%',
98+
backgroundColor: '#e9ecef',
99+
borderRadius: '4px',
100+
height: '20px',
101+
}}
102+
>
103+
<div
104+
style={{
105+
width: `${progress}%`,
106+
backgroundColor: '#007bff',
107+
height: '100%',
108+
}}
109+
/>
110+
</div>
111+
</div>
112+
)}
69113
</div>
70114
);
71115
}
@@ -81,14 +125,11 @@ function useWebWorker<T extends (...args: any[]) => any>(
81125
dependencies?: string[]; // 外部依赖
82126
localDependencies?: Function[]; // 本地函数依赖
83127
onError?: (error: Error) => void; // 错误回调
128+
onMessage?: (message: { type: string; data: any }) => void; // 自定义消息处理器
84129
}
85130
): [
86131
(...args: Parameters<T>) => Promise<ReturnType<T>>, // Worker 函数
87132
'PENDING' | 'RUNNING' | 'SUCCESS' | 'ERROR' | 'TIMEOUT_EXPIRED', // 状态
88133
(status?: WebWorkerStatus) => void, // 终止函数
89134
];
90135
```
91-
92-
## 开源协议
93-
94-
MIT

Diff for: packages/core/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@
2828
"prepublishOnly": "pnpm run build",
2929
"clean": "rm -rf dist",
3030
"release": "release-it",
31-
"release:minor": "release-it minor",
3231
"release:major": "release-it major",
32+
"release:minor": "release-it minor",
3333
"release:patch": "release-it patch"
3434
},
3535
"keywords": [

Diff for: packages/core/src/lib/createWorkerBlobUrl.ts

+28-23
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,38 @@
1+
export const enum WorkerMessageType {
2+
SUCCESS = 'SUCCESS',
3+
ERROR = 'ERROR',
4+
TIMEOUT_EXPIRED = 'TIMEOUT_EXPIRED',
5+
RUNNING = 'RUNNING',
6+
PENDING = 'PENDING',
7+
}
8+
19
export type WebWorkerStatus =
2-
| "PENDING"
3-
| "SUCCESS"
4-
| "RUNNING"
5-
| "ERROR"
6-
| "TIMEOUT_EXPIRED";
10+
| WorkerMessageType.PENDING
11+
| WorkerMessageType.SUCCESS
12+
| WorkerMessageType.ERROR
13+
| WorkerMessageType.TIMEOUT_EXPIRED
14+
| WorkerMessageType.RUNNING;
715

816
function createWorkerBlobUrl(
9-
fn: Function,
17+
fn: (...args: any[]) => any,
1018
dependencies: string[] = [],
11-
localDependencies: Function[] = []
19+
localDependencies: ((...args: any[]) => any)[] = []
1220
) {
1321
const blobCode = `
14-
${dependencies.map((d) => `importScripts("${d}");`).join("\n")}
15-
${localDependencies.map((f) => `${f.toString()}`).join("\n")}
16-
17-
const fnString = ${fn.toString()};
18-
19-
self.onmessage = async function(e) {
20-
const [args] = e.data;
21-
try {
22-
const result = await fnString.apply(null, args);
23-
self.postMessage(['SUCCESS', result]);
24-
} catch (error) {
25-
self.postMessage(['ERROR', error.message]);
26-
}
27-
}
28-
`;
22+
${dependencies.map(d => `importScripts("${d}");`).join('\n')}
23+
${localDependencies.map(f => `${f.toString()}`).join('\n')}
24+
const fnString = ${fn.toString()};
25+
self.onmessage = async function(e) {
26+
const [args] = e.data;
27+
try {
28+
const result = await fnString.apply(null, [...args, self]);
29+
self.postMessage(['${WorkerMessageType.SUCCESS}', result]);
30+
} catch (error) {
31+
self.postMessage(['${WorkerMessageType.ERROR}', error.message]);
32+
}
33+
}`;
2934

30-
const blob = new Blob([blobCode], { type: "text/javascript" });
35+
const blob = new Blob([blobCode], { type: 'text/javascript' });
3136
const url = URL.createObjectURL(blob);
3237

3338
return url;

Diff for: packages/core/src/useWebWorkerFn.ts

+21-14
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import { useState, useEffect, useRef } from 'react';
2-
import createWorkerBlobUrl, { WebWorkerStatus } from './lib/createWorkerBlobUrl.js';
2+
import createWorkerBlobUrl, {
3+
WebWorkerStatus,
4+
WorkerMessageType,
5+
} from './lib/createWorkerBlobUrl.js';
36

47
export interface UseWebWorkerFnOptions {
58
timeout?: number;
@@ -12,6 +15,7 @@ export interface UseWebWorkerFnOptions {
1215
*/
1316
localDependencies?: ((...args: unknown[]) => unknown)[];
1417
onError?: (error: Error) => void;
18+
onMessage?: (message: any) => void;
1519
}
1620

1721
export type UseWebWorkerFnReturn<T extends (...args: any[]) => any> = [
@@ -26,15 +30,15 @@ function useWebWorkerFn<T extends (...args: any[]) => any>(
2630
): UseWebWorkerFnReturn<T> {
2731
const { dependencies = [], localDependencies = [], timeout, onError } = options;
2832

29-
const [workerStatus, setWorkerStatus] = useState<WebWorkerStatus>('PENDING');
33+
const [workerStatus, setWorkerStatus] = useState<WebWorkerStatus>(WorkerMessageType.PENDING);
3034
const workerRef = useRef<(Worker & { _url?: string }) | null>(null);
3135
const promiseRef = useRef<{
3236
resolve?: (result: ReturnType<T>) => void;
3337
reject?: (error: Error) => void;
3438
}>({});
3539
const timeoutRef = useRef<number>();
3640

37-
const workerTerminate = (status: WebWorkerStatus = 'PENDING') => {
41+
const workerTerminate = (status: WebWorkerStatus = WorkerMessageType.PENDING) => {
3842
if (workerRef.current && workerRef.current._url) {
3943
workerRef.current.terminate();
4044
URL.revokeObjectURL(workerRef.current._url);
@@ -52,28 +56,31 @@ function useWebWorkerFn<T extends (...args: any[]) => any>(
5256

5357
newWorker.onmessage = (e: MessageEvent) => {
5458
const { resolve, reject } = promiseRef.current;
55-
const [status, result] = e.data as [WebWorkerStatus, ReturnType<T>];
59+
const [status, result] = e.data as [string, any];
5660

5761
switch (status) {
58-
case 'SUCCESS': {
62+
case WorkerMessageType.SUCCESS: {
5963
resolve?.(result);
60-
workerTerminate(status);
64+
workerTerminate(WorkerMessageType.SUCCESS);
6165
break;
6266
}
63-
case 'TIMEOUT_EXPIRED': {
67+
case WorkerMessageType.TIMEOUT_EXPIRED: {
6468
const timeoutError = new Error('Timeout');
6569
onError?.(timeoutError);
6670
reject?.(timeoutError);
67-
workerTerminate(status);
71+
workerTerminate(WorkerMessageType.TIMEOUT_EXPIRED);
6872
break;
6973
}
70-
default: {
74+
case WorkerMessageType.ERROR: {
7175
const error = new Error(result as string);
7276
onError?.(error);
7377
reject?.(error);
74-
workerTerminate('ERROR');
78+
workerTerminate(WorkerMessageType.ERROR);
7579
break;
7680
}
81+
default: {
82+
options.onMessage?.({ type: status, data: result });
83+
}
7784
}
7885
};
7986

@@ -83,14 +90,14 @@ function useWebWorkerFn<T extends (...args: any[]) => any>(
8390
const error = new Error(e.message);
8491
onError?.(error);
8592
reject?.(error);
86-
workerTerminate('ERROR');
93+
workerTerminate(WorkerMessageType.ERROR);
8794
};
8895

8996
if (timeout) {
9097
timeoutRef.current = window.setTimeout(() => {
9198
const { reject } = promiseRef.current;
9299
reject?.(new Error('Timeout'));
93-
workerTerminate('TIMEOUT_EXPIRED');
100+
workerTerminate(WorkerMessageType.TIMEOUT_EXPIRED);
94101
}, timeout);
95102
}
96103

@@ -101,11 +108,11 @@ function useWebWorkerFn<T extends (...args: any[]) => any>(
101108
new Promise<ReturnType<T>>((resolve, reject) => {
102109
promiseRef.current = { resolve, reject };
103110
workerRef.current?.postMessage([fnArgs]);
104-
setWorkerStatus('RUNNING');
111+
setWorkerStatus(WorkerMessageType.RUNNING);
105112
});
106113

107114
const workerFn = (...fnArgs: Parameters<T>) => {
108-
if (workerStatus === 'RUNNING') {
115+
if (workerStatus === WorkerMessageType.RUNNING) {
109116
console.error('[useWebWorkerFn] You can only run one instance of the worker at a time.');
110117
return Promise.reject(new Error('Worker is already running'));
111118
}

0 commit comments

Comments
 (0)