@yh-ui/request 0.1.21
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 +274 -0
- package/dist/adapters/fetch.cjs +157 -0
- package/dist/adapters/fetch.d.ts +25 -0
- package/dist/adapters/fetch.mjs +148 -0
- package/dist/adapters/index.cjs +27 -0
- package/dist/adapters/index.d.ts +5 -0
- package/dist/adapters/index.mjs +2 -0
- package/dist/adapters/platform.cjs +394 -0
- package/dist/adapters/platform.d.ts +72 -0
- package/dist/adapters/platform.mjs +369 -0
- package/dist/cache/index.cjs +56 -0
- package/dist/cache/index.d.ts +21 -0
- package/dist/cache/index.mjs +14 -0
- package/dist/cache/indexedDB.cjs +188 -0
- package/dist/cache/indexedDB.d.ts +58 -0
- package/dist/cache/indexedDB.mjs +176 -0
- package/dist/cache/localStorage.cjs +158 -0
- package/dist/cache/localStorage.d.ts +58 -0
- package/dist/cache/localStorage.mjs +153 -0
- package/dist/cache/memory.cjs +112 -0
- package/dist/cache/memory.d.ts +71 -0
- package/dist/cache/memory.mjs +103 -0
- package/dist/graphql.cjs +255 -0
- package/dist/graphql.d.ts +192 -0
- package/dist/graphql.mjs +235 -0
- package/dist/http-cache.cjs +248 -0
- package/dist/http-cache.d.ts +156 -0
- package/dist/http-cache.mjs +233 -0
- package/dist/index.cjs +181 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.mjs +16 -0
- package/dist/interceptors/debug.cjs +139 -0
- package/dist/interceptors/debug.d.ts +92 -0
- package/dist/interceptors/debug.mjs +130 -0
- package/dist/interceptors/index.cjs +38 -0
- package/dist/interceptors/index.d.ts +6 -0
- package/dist/interceptors/index.mjs +3 -0
- package/dist/interceptors/progress.cjs +185 -0
- package/dist/interceptors/progress.d.ts +97 -0
- package/dist/interceptors/progress.mjs +177 -0
- package/dist/interceptors/security.cjs +154 -0
- package/dist/interceptors/security.d.ts +83 -0
- package/dist/interceptors/security.mjs +134 -0
- package/dist/plugin.cjs +166 -0
- package/dist/plugin.d.ts +106 -0
- package/dist/plugin.mjs +163 -0
- package/dist/request.cjs +396 -0
- package/dist/request.d.ts +111 -0
- package/dist/request.mjs +339 -0
- package/dist/types.cjs +13 -0
- package/dist/types.d.ts +157 -0
- package/dist/types.mjs +7 -0
- package/dist/useAIStream.cjs +125 -0
- package/dist/useAIStream.d.ts +89 -0
- package/dist/useAIStream.mjs +108 -0
- package/dist/useLoadMore.cjs +136 -0
- package/dist/useLoadMore.d.ts +84 -0
- package/dist/useLoadMore.mjs +134 -0
- package/dist/usePagination.cjs +141 -0
- package/dist/usePagination.d.ts +89 -0
- package/dist/usePagination.mjs +132 -0
- package/dist/useQueue.cjs +243 -0
- package/dist/useQueue.d.ts +118 -0
- package/dist/useQueue.mjs +239 -0
- package/dist/useRequest.cjs +325 -0
- package/dist/useRequest.d.ts +126 -0
- package/dist/useRequest.mjs +329 -0
- package/dist/useRequestQueue.cjs +36 -0
- package/dist/useRequestQueue.d.ts +52 -0
- package/dist/useRequestQueue.mjs +27 -0
- package/dist/useSSE.cjs +241 -0
- package/dist/useSSE.d.ts +74 -0
- package/dist/useSSE.mjs +226 -0
- package/dist/websocket.cjs +325 -0
- package/dist/websocket.d.ts +163 -0
- package/dist/websocket.mjs +316 -0
- package/package.json +61 -0
package/dist/useSSE.d.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { type Ref, type ShallowRef } from 'vue';
|
|
2
|
+
import { type RequestOptions } from './types';
|
|
3
|
+
/** SSE 事件类型 */
|
|
4
|
+
export type SSEEventType = 'message' | 'start' | 'chunk' | 'done' | 'error' | 'tool' | 'thinking' | 'custom';
|
|
5
|
+
/** SSE 消息 */
|
|
6
|
+
export interface SSEMessage<T = unknown> {
|
|
7
|
+
/** 事件类型 */
|
|
8
|
+
event: SSEEventType;
|
|
9
|
+
/** 数据 */
|
|
10
|
+
data: T;
|
|
11
|
+
/** 原始行数据 */
|
|
12
|
+
raw: string;
|
|
13
|
+
/** 是否为最后一个消息 */
|
|
14
|
+
done: boolean;
|
|
15
|
+
/** 错误信息(如有) */
|
|
16
|
+
error?: Error;
|
|
17
|
+
}
|
|
18
|
+
/** SSE Hook 配置 */
|
|
19
|
+
export interface UseSSEOptions extends RequestOptions {
|
|
20
|
+
/** 自动解析 JSON */
|
|
21
|
+
parseJSON?: boolean;
|
|
22
|
+
/** 解码器 */
|
|
23
|
+
decoder?: TextDecoder;
|
|
24
|
+
/** 消息分隔符 */
|
|
25
|
+
separator?: string;
|
|
26
|
+
/** 自定义事件前缀 */
|
|
27
|
+
eventPrefix?: string;
|
|
28
|
+
/** 开始回调 */
|
|
29
|
+
onStart?: () => void;
|
|
30
|
+
/** 消息回调 */
|
|
31
|
+
onMessage?: (message: SSEMessage) => void;
|
|
32
|
+
/** 完成回调 */
|
|
33
|
+
onDone?: (fullContent: string) => void;
|
|
34
|
+
/** 错误回调 */
|
|
35
|
+
onError?: (error: Error) => void;
|
|
36
|
+
/** 自定义事件回调 */
|
|
37
|
+
onCustomEvent?: (event: string, data: unknown) => void;
|
|
38
|
+
}
|
|
39
|
+
/** SSE Hook 返回值 */
|
|
40
|
+
export interface UseSSEReturn {
|
|
41
|
+
/** 是否正在连接/接收 */
|
|
42
|
+
loading: Ref<boolean>;
|
|
43
|
+
/** 当前接收到的文本 */
|
|
44
|
+
content: Ref<string>;
|
|
45
|
+
/** 完整消息列表 */
|
|
46
|
+
messages: Ref<SSEMessage[]>;
|
|
47
|
+
/** 错误 */
|
|
48
|
+
error: ShallowRef<Error | undefined>;
|
|
49
|
+
/** 启动流式请求 */
|
|
50
|
+
start: (options?: RequestOptions) => void;
|
|
51
|
+
/** 停止流式请求 */
|
|
52
|
+
stop: () => void;
|
|
53
|
+
/** 重置状态 */
|
|
54
|
+
reset: () => void;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* useSSE - Server-Sent Events 流式请求 Hook
|
|
58
|
+
*
|
|
59
|
+
* 适用于 AI 应用中的流式响应
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* const { content, loading, messages, start, stop } = useSSE({
|
|
63
|
+
* onMessage: (msg) => console.log(msg.data),
|
|
64
|
+
* onDone: (full) => console.log('Complete:', full),
|
|
65
|
+
* })
|
|
66
|
+
*
|
|
67
|
+
* // 发起请求
|
|
68
|
+
* start({
|
|
69
|
+
* url: '/api/chat',
|
|
70
|
+
* method: 'POST',
|
|
71
|
+
* data: { messages: [...] },
|
|
72
|
+
* })
|
|
73
|
+
*/
|
|
74
|
+
export declare function useSSE(options?: UseSSEOptions): UseSSEReturn;
|
package/dist/useSSE.mjs
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { ref, shallowRef, onUnmounted } from "vue";
|
|
2
|
+
export function useSSE(options = {}) {
|
|
3
|
+
const {
|
|
4
|
+
parseJSON = true,
|
|
5
|
+
separator = "\n\n",
|
|
6
|
+
eventPrefix = "event:",
|
|
7
|
+
onStart,
|
|
8
|
+
onMessage,
|
|
9
|
+
onDone,
|
|
10
|
+
onError,
|
|
11
|
+
onCustomEvent,
|
|
12
|
+
...fetchOptions
|
|
13
|
+
} = options;
|
|
14
|
+
const loading = ref(false);
|
|
15
|
+
const content = ref("");
|
|
16
|
+
const messages = ref([]);
|
|
17
|
+
const error = shallowRef(void 0);
|
|
18
|
+
let abortController = null;
|
|
19
|
+
let reader = null;
|
|
20
|
+
let decoder;
|
|
21
|
+
const getDecoder = () => {
|
|
22
|
+
if (!decoder) {
|
|
23
|
+
decoder = new TextDecoder();
|
|
24
|
+
}
|
|
25
|
+
return decoder;
|
|
26
|
+
};
|
|
27
|
+
const parseLine = (line) => {
|
|
28
|
+
const result = { data: "" };
|
|
29
|
+
if (!line || line.startsWith("#")) return result;
|
|
30
|
+
if (line.startsWith(eventPrefix)) {
|
|
31
|
+
result.event = line.slice(eventPrefix.length).trim();
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
if (line.startsWith("data:")) {
|
|
35
|
+
const data = line.slice(5);
|
|
36
|
+
result.data = data.startsWith(" ") ? data.slice(1) : data;
|
|
37
|
+
}
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
const parseSSE = (rawData) => {
|
|
41
|
+
const lines = rawData.split("\n");
|
|
42
|
+
let originalEventType = "message";
|
|
43
|
+
let eventData = "";
|
|
44
|
+
let done = false;
|
|
45
|
+
for (const line of lines) {
|
|
46
|
+
const { event, data } = parseLine(line);
|
|
47
|
+
if (event) originalEventType = event;
|
|
48
|
+
if (data) eventData += data;
|
|
49
|
+
}
|
|
50
|
+
let eventType = "message";
|
|
51
|
+
switch (originalEventType.toLowerCase()) {
|
|
52
|
+
case "start":
|
|
53
|
+
eventType = "start";
|
|
54
|
+
break;
|
|
55
|
+
case "message":
|
|
56
|
+
case "chunk":
|
|
57
|
+
case "content":
|
|
58
|
+
eventType = "chunk";
|
|
59
|
+
break;
|
|
60
|
+
case "done":
|
|
61
|
+
case "stop":
|
|
62
|
+
eventType = "done";
|
|
63
|
+
done = true;
|
|
64
|
+
break;
|
|
65
|
+
case "error":
|
|
66
|
+
eventType = "error";
|
|
67
|
+
break;
|
|
68
|
+
case "tool":
|
|
69
|
+
case "function":
|
|
70
|
+
eventType = "tool";
|
|
71
|
+
break;
|
|
72
|
+
case "thinking":
|
|
73
|
+
case "thinking_start":
|
|
74
|
+
case "thinking_end":
|
|
75
|
+
eventType = "thinking";
|
|
76
|
+
break;
|
|
77
|
+
default:
|
|
78
|
+
eventType = "custom";
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
if (!eventData && eventType !== "done" && eventType !== "start") return null;
|
|
82
|
+
let finalData = eventData;
|
|
83
|
+
if (parseJSON && eventData) {
|
|
84
|
+
try {
|
|
85
|
+
finalData = JSON.parse(eventData);
|
|
86
|
+
} catch {
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (eventType === "custom" && onCustomEvent) {
|
|
90
|
+
onCustomEvent(originalEventType, finalData);
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
event: eventType,
|
|
94
|
+
data: finalData,
|
|
95
|
+
raw: rawData,
|
|
96
|
+
done
|
|
97
|
+
};
|
|
98
|
+
};
|
|
99
|
+
const start = async (requestOptions) => {
|
|
100
|
+
stop();
|
|
101
|
+
reset();
|
|
102
|
+
const config = {
|
|
103
|
+
...fetchOptions,
|
|
104
|
+
...requestOptions
|
|
105
|
+
};
|
|
106
|
+
loading.value = true;
|
|
107
|
+
error.value = void 0;
|
|
108
|
+
onStart?.();
|
|
109
|
+
try {
|
|
110
|
+
let url = config.url || "";
|
|
111
|
+
if (config.baseURL) {
|
|
112
|
+
url = url.startsWith("http") ? url : config.baseURL + url;
|
|
113
|
+
}
|
|
114
|
+
if (config.params) {
|
|
115
|
+
const params = new URLSearchParams();
|
|
116
|
+
Object.entries(config.params).forEach(([key, value]) => {
|
|
117
|
+
if (Array.isArray(value)) {
|
|
118
|
+
value.forEach((v) => params.append(key, String(v)));
|
|
119
|
+
} else {
|
|
120
|
+
params.append(key, String(value));
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
const sep = url.includes("?") ? "&" : "?";
|
|
124
|
+
url += sep + params.toString();
|
|
125
|
+
}
|
|
126
|
+
const headers = config.headers || {};
|
|
127
|
+
const fetchInit = {
|
|
128
|
+
method: config.method || "POST",
|
|
129
|
+
headers,
|
|
130
|
+
credentials: config.credentials || "same-origin"
|
|
131
|
+
};
|
|
132
|
+
if (config.data && config.method !== "GET") {
|
|
133
|
+
fetchInit.body = JSON.stringify(config.data);
|
|
134
|
+
if (!headers["Content-Type"]) {
|
|
135
|
+
headers["Content-Type"] = "application/json";
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
abortController = new AbortController();
|
|
139
|
+
fetchInit.signal = abortController.signal;
|
|
140
|
+
const response = await fetch(url, fetchInit);
|
|
141
|
+
if (!response.ok) {
|
|
142
|
+
throw new Error(`SSE request failed: ${response.status} ${response.statusText}`);
|
|
143
|
+
}
|
|
144
|
+
const body = response.body;
|
|
145
|
+
if (!body) {
|
|
146
|
+
throw new Error("Response body is null");
|
|
147
|
+
}
|
|
148
|
+
reader = body.getReader();
|
|
149
|
+
const textDecoder = getDecoder();
|
|
150
|
+
let buffer = "";
|
|
151
|
+
while (true) {
|
|
152
|
+
const { done: readDone, value } = await reader.read();
|
|
153
|
+
if (readDone) {
|
|
154
|
+
if (buffer) {
|
|
155
|
+
const msg = parseSSE(buffer);
|
|
156
|
+
if (msg) {
|
|
157
|
+
handleMessage(msg);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
const chunk = textDecoder.decode(value, { stream: true });
|
|
163
|
+
buffer += chunk;
|
|
164
|
+
const parts = buffer.split(separator);
|
|
165
|
+
buffer = parts.pop() || "";
|
|
166
|
+
for (const part of parts) {
|
|
167
|
+
const msg = parseSSE(part);
|
|
168
|
+
if (msg) {
|
|
169
|
+
handleMessage(msg);
|
|
170
|
+
if (msg.done) {
|
|
171
|
+
onDone?.(content.value);
|
|
172
|
+
loading.value = false;
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
onDone?.(content.value);
|
|
179
|
+
} catch (err) {
|
|
180
|
+
const errWithName = err;
|
|
181
|
+
if (errWithName?.name === "AbortError") {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const errObj = err instanceof Error ? err : new Error(String(err));
|
|
185
|
+
error.value = errObj;
|
|
186
|
+
onError?.(errObj);
|
|
187
|
+
} finally {
|
|
188
|
+
loading.value = false;
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
const handleMessage = (message) => {
|
|
192
|
+
if (typeof message.data === "string") {
|
|
193
|
+
content.value += message.data;
|
|
194
|
+
}
|
|
195
|
+
messages.value.push(message);
|
|
196
|
+
onMessage?.(message);
|
|
197
|
+
};
|
|
198
|
+
const stop = () => {
|
|
199
|
+
if (reader) {
|
|
200
|
+
reader.cancel();
|
|
201
|
+
reader = null;
|
|
202
|
+
}
|
|
203
|
+
if (abortController) {
|
|
204
|
+
abortController.abort();
|
|
205
|
+
abortController = null;
|
|
206
|
+
}
|
|
207
|
+
loading.value = false;
|
|
208
|
+
};
|
|
209
|
+
const reset = () => {
|
|
210
|
+
content.value = "";
|
|
211
|
+
messages.value = [];
|
|
212
|
+
error.value = void 0;
|
|
213
|
+
};
|
|
214
|
+
onUnmounted(() => {
|
|
215
|
+
stop();
|
|
216
|
+
});
|
|
217
|
+
return {
|
|
218
|
+
loading,
|
|
219
|
+
content,
|
|
220
|
+
messages,
|
|
221
|
+
error,
|
|
222
|
+
start,
|
|
223
|
+
stop,
|
|
224
|
+
reset
|
|
225
|
+
};
|
|
226
|
+
}
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.WebSocketClient = void 0;
|
|
7
|
+
exports.createWebSocket = createWebSocket;
|
|
8
|
+
exports.isWebSocketSupported = isWebSocketSupported;
|
|
9
|
+
exports.useWebSocket = useWebSocket;
|
|
10
|
+
var _vue = require("vue");
|
|
11
|
+
class WebSocketClient {
|
|
12
|
+
ws = null;
|
|
13
|
+
url = "";
|
|
14
|
+
protocols;
|
|
15
|
+
options;
|
|
16
|
+
// 状态
|
|
17
|
+
_state = "disconnected";
|
|
18
|
+
reconnectAttempts = 0;
|
|
19
|
+
reconnectTimer = null;
|
|
20
|
+
heartbeatTimer = null;
|
|
21
|
+
heartbeatTimeoutTimer = null;
|
|
22
|
+
messageQueue = [];
|
|
23
|
+
// 回调
|
|
24
|
+
onOpenCallback = null;
|
|
25
|
+
onCloseCallback = null;
|
|
26
|
+
onErrorCallback = null;
|
|
27
|
+
onMessageCallback = null;
|
|
28
|
+
onStateChangeCallback = null;
|
|
29
|
+
// 响应式状态 (供 Vue 使用)
|
|
30
|
+
state;
|
|
31
|
+
isConnected;
|
|
32
|
+
lastMessage;
|
|
33
|
+
constructor(options) {
|
|
34
|
+
this.url = options.url;
|
|
35
|
+
this.protocols = options.protocols;
|
|
36
|
+
this.options = {
|
|
37
|
+
url: options.url,
|
|
38
|
+
protocols: options.protocols,
|
|
39
|
+
reconnect: options.reconnect ?? true,
|
|
40
|
+
reconnectMaxAttempts: options.reconnectMaxAttempts ?? 10,
|
|
41
|
+
reconnectInterval: options.reconnectInterval ?? 1e3,
|
|
42
|
+
reconnectMaxDelay: options.reconnectMaxDelay ?? 3e4,
|
|
43
|
+
heartbeat: options.heartbeat ?? false,
|
|
44
|
+
heartbeatInterval: options.heartbeatInterval ?? 3e4,
|
|
45
|
+
heartbeatTimeout: options.heartbeatTimeout ?? 1e4,
|
|
46
|
+
headers: options.headers ?? {},
|
|
47
|
+
binaryType: options.binaryType ?? "blob",
|
|
48
|
+
encode: options.encode ?? (data => JSON.stringify(data)),
|
|
49
|
+
decode: options.decode ?? (data => {
|
|
50
|
+
if (typeof data === "string") {
|
|
51
|
+
try {
|
|
52
|
+
return JSON.parse(data);
|
|
53
|
+
} catch {
|
|
54
|
+
return data;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return data;
|
|
58
|
+
})
|
|
59
|
+
};
|
|
60
|
+
this.state = (0, _vue.ref)("disconnected");
|
|
61
|
+
this.isConnected = (0, _vue.ref)(false);
|
|
62
|
+
this.lastMessage = (0, _vue.ref)(null);
|
|
63
|
+
this.handleOpen = this.handleOpen.bind(this);
|
|
64
|
+
this.handleClose = this.handleClose.bind(this);
|
|
65
|
+
this.handleError = this.handleError.bind(this);
|
|
66
|
+
this.handleMessage = this.handleMessage.bind(this);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* 获取当前状态
|
|
70
|
+
*/
|
|
71
|
+
getState() {
|
|
72
|
+
return this._state;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* 设置状态
|
|
76
|
+
*/
|
|
77
|
+
setState(newState) {
|
|
78
|
+
this._state = newState;
|
|
79
|
+
this.state.value = newState;
|
|
80
|
+
this.isConnected.value = newState === "connected";
|
|
81
|
+
this.onStateChangeCallback?.(newState);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* 连接
|
|
85
|
+
*/
|
|
86
|
+
connect() {
|
|
87
|
+
return new Promise((resolve, reject) => {
|
|
88
|
+
if (!isWebSocketSupported()) {
|
|
89
|
+
const error = new Error("WebSocket is not supported in this environment");
|
|
90
|
+
this.setState("error");
|
|
91
|
+
reject(error);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const CONNECTING = 0;
|
|
95
|
+
const OPEN = 1;
|
|
96
|
+
if (this.ws && (this.ws.readyState === OPEN || this.ws.readyState === CONNECTING)) {
|
|
97
|
+
resolve();
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
this.setState("connecting");
|
|
101
|
+
try {
|
|
102
|
+
this.ws = this.protocols ? new WebSocket(this.url, this.protocols) : new WebSocket(this.url);
|
|
103
|
+
this.ws.binaryType = this.options.binaryType;
|
|
104
|
+
this.ws.onopen = event => {
|
|
105
|
+
this.handleOpen(event);
|
|
106
|
+
resolve();
|
|
107
|
+
};
|
|
108
|
+
this.ws.onclose = this.handleClose;
|
|
109
|
+
this.ws.onerror = this.handleError;
|
|
110
|
+
this.ws.onmessage = this.handleMessage;
|
|
111
|
+
} catch (error) {
|
|
112
|
+
this.setState("error");
|
|
113
|
+
reject(error);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* 断开连接
|
|
119
|
+
*/
|
|
120
|
+
disconnect(code = 1e3, reason = "Client disconnect") {
|
|
121
|
+
this.clearTimers();
|
|
122
|
+
this.setState("disconnecting");
|
|
123
|
+
if (this.ws) {
|
|
124
|
+
this.ws.close(code, reason);
|
|
125
|
+
this.ws = null;
|
|
126
|
+
}
|
|
127
|
+
this.setState("disconnected");
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* 发送消息
|
|
131
|
+
*/
|
|
132
|
+
send(data) {
|
|
133
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
134
|
+
this.messageQueue.push(this.options.encode(data));
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
const encoded = this.options.encode(data);
|
|
138
|
+
this.ws.send(encoded);
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* 发送并等待响应
|
|
143
|
+
*/
|
|
144
|
+
sendAndWait(data, timeout = 3e4) {
|
|
145
|
+
return new Promise((resolve, reject) => {
|
|
146
|
+
const messageId = Date.now().toString();
|
|
147
|
+
const timer = setTimeout(() => {
|
|
148
|
+
reject(new Error("WebSocket message timeout"));
|
|
149
|
+
}, timeout);
|
|
150
|
+
const originalCallback = this.onMessageCallback;
|
|
151
|
+
this.onMessageCallback = message => {
|
|
152
|
+
const decoded = message.data;
|
|
153
|
+
if (decoded && decoded.id === messageId) {
|
|
154
|
+
clearTimeout(timer);
|
|
155
|
+
this.onMessageCallback = originalCallback;
|
|
156
|
+
resolve(decoded.result);
|
|
157
|
+
} else {
|
|
158
|
+
originalCallback?.(message);
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
this.send({
|
|
162
|
+
id: messageId,
|
|
163
|
+
...data
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* 事件回调
|
|
169
|
+
*/
|
|
170
|
+
onOpen(callback) {
|
|
171
|
+
this.onOpenCallback = callback;
|
|
172
|
+
return this;
|
|
173
|
+
}
|
|
174
|
+
onClose(callback) {
|
|
175
|
+
this.onCloseCallback = callback;
|
|
176
|
+
return this;
|
|
177
|
+
}
|
|
178
|
+
onError(callback) {
|
|
179
|
+
this.onErrorCallback = callback;
|
|
180
|
+
return this;
|
|
181
|
+
}
|
|
182
|
+
onMessage(callback) {
|
|
183
|
+
this.onMessageCallback = callback;
|
|
184
|
+
return this;
|
|
185
|
+
}
|
|
186
|
+
onStateChange(callback) {
|
|
187
|
+
this.onStateChangeCallback = callback;
|
|
188
|
+
return this;
|
|
189
|
+
}
|
|
190
|
+
// ==================== 内部方法 ====================
|
|
191
|
+
handleOpen(_event) {
|
|
192
|
+
this.setState("connected");
|
|
193
|
+
this.reconnectAttempts = 0;
|
|
194
|
+
while (this.messageQueue.length > 0) {
|
|
195
|
+
const data = this.messageQueue.shift();
|
|
196
|
+
if (data && this.ws) {
|
|
197
|
+
this.ws.send(data);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (this.options.heartbeat) {
|
|
201
|
+
this.startHeartbeat();
|
|
202
|
+
}
|
|
203
|
+
this.onOpenCallback?.();
|
|
204
|
+
}
|
|
205
|
+
handleClose(event) {
|
|
206
|
+
this.ws = null;
|
|
207
|
+
this.clearTimers();
|
|
208
|
+
this.setState("disconnected");
|
|
209
|
+
if (this.options.reconnect && this.reconnectAttempts < this.options.reconnectMaxAttempts) {
|
|
210
|
+
this.reconnect();
|
|
211
|
+
}
|
|
212
|
+
this.onCloseCallback?.(event.code, event.reason);
|
|
213
|
+
}
|
|
214
|
+
handleError(event) {
|
|
215
|
+
this.setState("error");
|
|
216
|
+
this.onErrorCallback?.(event);
|
|
217
|
+
}
|
|
218
|
+
handleMessage(event) {
|
|
219
|
+
const raw = event.data;
|
|
220
|
+
let data;
|
|
221
|
+
if (event.data === "pong") {
|
|
222
|
+
this.clearHeartbeatTimeout();
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
try {
|
|
226
|
+
data = this.options.decode(raw);
|
|
227
|
+
} catch {
|
|
228
|
+
data = raw;
|
|
229
|
+
}
|
|
230
|
+
const message = {
|
|
231
|
+
type: event.data instanceof ArrayBuffer || event.data instanceof Blob ? "binary" : "text",
|
|
232
|
+
data,
|
|
233
|
+
raw,
|
|
234
|
+
timestamp: Date.now()
|
|
235
|
+
};
|
|
236
|
+
this.lastMessage.value = message;
|
|
237
|
+
this.onMessageCallback?.(message);
|
|
238
|
+
}
|
|
239
|
+
reconnect() {
|
|
240
|
+
this.setState("reconnecting");
|
|
241
|
+
this.reconnectAttempts++;
|
|
242
|
+
const delay = Math.min(this.options.reconnectInterval * Math.pow(2, this.reconnectAttempts - 1), this.options.reconnectMaxDelay);
|
|
243
|
+
this.reconnectTimer = setTimeout(() => {
|
|
244
|
+
this.connect().catch(() => {});
|
|
245
|
+
}, delay);
|
|
246
|
+
}
|
|
247
|
+
startHeartbeat() {
|
|
248
|
+
this.heartbeatTimer = setInterval(() => {
|
|
249
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
250
|
+
this.ws.send("ping");
|
|
251
|
+
this.heartbeatTimeoutTimer = setTimeout(() => {
|
|
252
|
+
this.disconnect(4e3, "Heartbeat timeout");
|
|
253
|
+
}, this.options.heartbeatTimeout);
|
|
254
|
+
}
|
|
255
|
+
}, this.options.heartbeatInterval);
|
|
256
|
+
}
|
|
257
|
+
clearHeartbeatTimeout() {
|
|
258
|
+
if (this.heartbeatTimeoutTimer) {
|
|
259
|
+
clearTimeout(this.heartbeatTimeoutTimer);
|
|
260
|
+
this.heartbeatTimeoutTimer = null;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
clearTimers() {
|
|
264
|
+
if (this.reconnectTimer) {
|
|
265
|
+
clearTimeout(this.reconnectTimer);
|
|
266
|
+
this.reconnectTimer = null;
|
|
267
|
+
}
|
|
268
|
+
if (this.heartbeatTimer) {
|
|
269
|
+
clearInterval(this.heartbeatTimer);
|
|
270
|
+
this.heartbeatTimer = null;
|
|
271
|
+
}
|
|
272
|
+
this.clearHeartbeatTimeout();
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* 清理资源
|
|
276
|
+
*/
|
|
277
|
+
dispose() {
|
|
278
|
+
this.disconnect();
|
|
279
|
+
this.onOpenCallback = null;
|
|
280
|
+
this.onCloseCallback = null;
|
|
281
|
+
this.onErrorCallback = null;
|
|
282
|
+
this.onMessageCallback = null;
|
|
283
|
+
this.onStateChangeCallback = null;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
exports.WebSocketClient = WebSocketClient;
|
|
287
|
+
function useWebSocket(options) {
|
|
288
|
+
const client = new WebSocketClient(options);
|
|
289
|
+
const state = client.state;
|
|
290
|
+
const isConnected = client.isConnected;
|
|
291
|
+
const lastMessage = client.lastMessage;
|
|
292
|
+
(0, _vue.onUnmounted)(() => {
|
|
293
|
+
client.dispose();
|
|
294
|
+
});
|
|
295
|
+
return {
|
|
296
|
+
/** 当前状态 */
|
|
297
|
+
state,
|
|
298
|
+
/** 是否已连接 */
|
|
299
|
+
isConnected,
|
|
300
|
+
/** 最后收到的消息 */
|
|
301
|
+
lastMessage,
|
|
302
|
+
/** 连接 */
|
|
303
|
+
connect: () => client.connect(),
|
|
304
|
+
/** 断开连接 */
|
|
305
|
+
disconnect: (code, reason) => client.disconnect(code, reason),
|
|
306
|
+
/** 发送消息 */
|
|
307
|
+
send: data => client.send(data),
|
|
308
|
+
/** 发送并等待响应 */
|
|
309
|
+
sendAndWait: (data, timeout) => client.sendAndWait(data, timeout),
|
|
310
|
+
/** 事件监听 */
|
|
311
|
+
onOpen: callback => client.onOpen(callback),
|
|
312
|
+
onClose: callback => client.onClose(callback),
|
|
313
|
+
onError: callback => client.onError(callback),
|
|
314
|
+
onMessage: callback => client.onMessage(callback),
|
|
315
|
+
onStateChange: callback => client.onStateChange(callback),
|
|
316
|
+
/** 获取原始客户端 */
|
|
317
|
+
getClient: () => client
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
function createWebSocket(options) {
|
|
321
|
+
return new WebSocketClient(options);
|
|
322
|
+
}
|
|
323
|
+
function isWebSocketSupported() {
|
|
324
|
+
return typeof WebSocket !== "undefined";
|
|
325
|
+
}
|