Skip to content

Commit 6302502

Browse files
committed
feat: add useSSE and useAudioPlayer
1 parent 5846c81 commit 6302502

File tree

10 files changed

+674
-104
lines changed

10 files changed

+674
-104
lines changed

CHANGELOG.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,28 @@
22

33
# Changelog
44

5+
## 1.4.9
6+
7+
2025-9-2
8+
9+
### Features
10+
11+
#### hooks
12+
13+
- ✨ add a new hook `useSSE` which provides a simple way to connect and communicate with a Server-Sent Events (SSE) server.
14+
- ✨ add a new hook `useAudioPlayer` used to play audio from a given source URL or streaming data.
15+
- ✨ add a new util `utils.arrayBufferToBase64` used to convert an ArrayBuffer to a Base64 encoded string.
16+
- ✨ add a new util `utils.base64ToArrayBuffer` used to convert a Base64 encoded string to an ArrayBuffer.
17+
18+
### Notable Changes
19+
20+
- 🔥 `utils.toBase64` method is renamed to `utils.stringToBase64`.
21+
- 🔥 `utils.fromBase64` method is renamed to `utils.base64ToString`.
22+
23+
### Bug Fixes
24+
25+
- 🐞 Fix sourcemap are not referenced correctly in `assets`
26+
527
## 1.4.8
628

729
2025-9-1

package-lock.json

Lines changed: 9 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@tiny-codes/react-easy",
3-
"version": "1.4.8",
3+
"version": "1.4.9",
44
"description": "Simplify React and AntDesign development with practical components and hooks",
55
"keywords": [
66
"react",
@@ -39,6 +39,7 @@
3939
},
4040
"dependencies": {
4141
"@lexical/react": "^0.33.1",
42+
"@microsoft/fetch-event-source": "^2.0.1",
4243
"@stomp/stompjs": "^7.1.1",
4344
"i18next": "^24.2.3",
4445
"lexical": "^0.33.1",

src/hooks/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
export { default as useAudioPlayer } from './useAudioPlayer';
12
export { default as useRefValue } from './useRefValue';
23
export { default as useRefFunction } from './useRefFunction';
4+
export * from './useSSE';
5+
export { default as useSSE } from './useSSE';
36
export * from './useStompSocket';
47
export { default as useStompSocket } from './useStompSocket';
58
export type { ValidatorRuleMap } from './useValidators';

src/hooks/useAudioPlayer.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { useRef } from 'react';
2+
import AudioPlayer, { type AudioPlayerInit } from '../utils/AudioPlayer';
3+
4+
/**
5+
* - **EN:** A hook that provides an instance of the AudioPlayer class for managing audio playback.
6+
* - **CN:** 一个提供AudioPlayer类实例的钩子,用于管理音频播放。
7+
*/
8+
const useAudioPlayer = (props?: AudioPlayerInit): AudioPlayer => {
9+
const ref = useRef<AudioPlayer>(new AudioPlayer(props));
10+
return ref.current;
11+
};
12+
13+
export default useAudioPlayer;

src/hooks/useSSE.ts

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import { useEffect, useRef, useState } from 'react';
2+
import type { EventSourceMessage, FetchEventSourceInit } from '@microsoft/fetch-event-source';
3+
import { fetchEventSource } from '@microsoft/fetch-event-source';
4+
import useRefFunction from './useRefFunction';
5+
import useRefValue from './useRefValue';
6+
7+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
8+
export interface UseSSEProps<T = any> {
9+
/** The URL to connect to */
10+
url: RequestInfo;
11+
/** Options for the connection. */
12+
connectOptions?: Omit<FetchEventSourceInit, 'onmessage' | 'onerror' | 'onclose'>;
13+
/** Automatically connect to the SSE channel. Default is `false`. */
14+
autoConnect?: boolean;
15+
/** Automatically close the connection when the component unmounts. Default is `true`. */
16+
autoClose?: boolean;
17+
/** Function to parse the incoming message. If not provided, the default JSON parser will be used. */
18+
parseMessage?: (original: EventSourceMessage) => T;
19+
/** Callback function to handle incoming messages. */
20+
onMessage?: (messageData: T) => void;
21+
/** Callback function to handle errors. */
22+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
23+
onError?: (error: any) => void;
24+
/** Callback function to handle connection closure. */
25+
onClose?: () => void;
26+
}
27+
28+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
29+
const useSSE = <T = any>(props: UseSSEProps<T>) => {
30+
const { url, autoConnect, connectOptions, onMessage, parseMessage, onError, onClose } = props;
31+
const autoConnectRef = useRefValue(autoConnect);
32+
const [isRequesting, setIsRequesting] = useState(false);
33+
const isRequestingRef = useRefValue(isRequesting);
34+
const [isConnected, setIsConnected] = useState<boolean>(false);
35+
const abortCtrlRef = useRef<AbortController | undefined>(undefined);
36+
37+
const connect = useRefFunction(async (options?: FetchEventSourceInit & Partial<Pick<UseSSEProps, 'url'>>) => {
38+
const {
39+
url: connectUrl = url,
40+
headers = {},
41+
onopen,
42+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
43+
onmessage,
44+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
45+
onclose,
46+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
47+
onerror,
48+
...restOptions
49+
} = { ...connectOptions, ...options };
50+
abortCtrlRef.current = new AbortController();
51+
try {
52+
setIsRequesting(true);
53+
setIsConnected(false);
54+
await fetchEventSource(connectUrl, {
55+
method: 'post',
56+
signal: abortCtrlRef.current.signal,
57+
openWhenHidden: true,
58+
onopen: async (response: Response) => {
59+
setIsConnected(true);
60+
onopen?.(response);
61+
},
62+
onmessage(event) {
63+
if (!isRequestingRef.current) {
64+
setIsRequesting(false);
65+
}
66+
try {
67+
let parsed: T;
68+
if (parseMessage) {
69+
parsed = parseMessage(event);
70+
} else {
71+
parsed = event.data ? JSON.parse(event.data) : undefined;
72+
}
73+
if (parsed != null) {
74+
onMessage?.(parsed);
75+
}
76+
} catch (error) {
77+
console.error('Error parsing message data:', error);
78+
console.log('The underlying event:', event);
79+
}
80+
},
81+
onerror(error) {
82+
setIsRequesting(false);
83+
onError?.(error);
84+
},
85+
onclose() {
86+
setIsRequesting(false);
87+
onClose?.();
88+
},
89+
headers: {
90+
'Content-Type': 'application/json',
91+
...headers,
92+
},
93+
...restOptions,
94+
});
95+
} finally {
96+
setIsRequesting(false);
97+
}
98+
});
99+
100+
const close = useRefFunction(() => {
101+
setIsConnected(false);
102+
if (isConnected && abortCtrlRef.current) {
103+
abortCtrlRef.current.abort();
104+
}
105+
});
106+
107+
useEffect(() => {
108+
if (autoConnectRef.current) {
109+
connect();
110+
}
111+
return close;
112+
}, []);
113+
114+
// 清理函数
115+
return {
116+
connect,
117+
close,
118+
isRequesting,
119+
isConnected,
120+
};
121+
};
122+
123+
export default useSSE;

0 commit comments

Comments
 (0)