@kevisual/router 0.0.66 → 0.0.68
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/dist/opencode.d.ts +21 -3
- package/dist/router-browser.d.ts +26 -18
- package/dist/router-browser.js +58 -10
- package/dist/router.d.ts +26 -18
- package/dist/router.js +58 -10
- package/dist/ws.d.ts +734 -0
- package/dist/ws.js +186 -0
- package/package.json +11 -11
- package/src/browser.ts +1 -1
- package/src/index.ts +1 -1
- package/src/route.ts +59 -9
- package/src/server/reconnect-ws.ts +170 -0
- package/src/ws.ts +65 -0
package/dist/ws.js
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import WebSocket from 'ws';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 一个支持自动重连的 WebSocket 客户端。
|
|
5
|
+
* 在连接断开时会根据配置进行重连尝试,支持指数退避。
|
|
6
|
+
*/
|
|
7
|
+
class ReconnectingWebSocket {
|
|
8
|
+
ws = null;
|
|
9
|
+
url;
|
|
10
|
+
config;
|
|
11
|
+
retryCount = 0;
|
|
12
|
+
reconnectTimer = null;
|
|
13
|
+
isManualClose = false;
|
|
14
|
+
messageHandlers = [];
|
|
15
|
+
openHandlers = [];
|
|
16
|
+
closeHandlers = [];
|
|
17
|
+
errorHandlers = [];
|
|
18
|
+
constructor(url, config = {}) {
|
|
19
|
+
this.url = url;
|
|
20
|
+
this.config = {
|
|
21
|
+
maxRetries: config.maxRetries ?? Infinity,
|
|
22
|
+
retryDelay: config.retryDelay ?? 1000,
|
|
23
|
+
maxDelay: config.maxDelay ?? 30000,
|
|
24
|
+
backoffMultiplier: config.backoffMultiplier ?? 2,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
log(...args) {
|
|
28
|
+
console.log('[ReconnectingWebSocket]', ...args);
|
|
29
|
+
}
|
|
30
|
+
error(...args) {
|
|
31
|
+
console.error('[ReconnectingWebSocket]', ...args);
|
|
32
|
+
}
|
|
33
|
+
connect() {
|
|
34
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
this.log(`正在连接到 ${this.url}...`);
|
|
38
|
+
this.ws = new WebSocket(this.url);
|
|
39
|
+
this.ws.on('open', () => {
|
|
40
|
+
this.log('WebSocket 连接已打开');
|
|
41
|
+
this.retryCount = 0;
|
|
42
|
+
this.openHandlers.forEach(handler => handler());
|
|
43
|
+
this.send({ type: 'heartbeat', timestamp: new Date().toISOString() });
|
|
44
|
+
});
|
|
45
|
+
this.ws.on('message', (data) => {
|
|
46
|
+
this.messageHandlers.forEach(handler => {
|
|
47
|
+
try {
|
|
48
|
+
const message = JSON.parse(data.toString());
|
|
49
|
+
handler(message);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
handler(data.toString());
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
this.ws.on('close', (code, reason) => {
|
|
57
|
+
this.log(`WebSocket 连接已关闭: code=${code}, reason=${reason.toString()}`);
|
|
58
|
+
this.closeHandlers.forEach(handler => handler(code, reason));
|
|
59
|
+
if (!this.isManualClose) {
|
|
60
|
+
this.scheduleReconnect();
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
this.ws.on('error', (error) => {
|
|
64
|
+
this.error('WebSocket 错误:', error.message);
|
|
65
|
+
this.errorHandlers.forEach(handler => handler(error));
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
scheduleReconnect() {
|
|
69
|
+
if (this.reconnectTimer) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (this.retryCount >= this.config.maxRetries) {
|
|
73
|
+
this.error(`已达到最大重试次数 (${this.config.maxRetries}),停止重连`);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
// 计算延迟(指数退避)
|
|
77
|
+
const delay = Math.min(this.config.retryDelay * Math.pow(this.config.backoffMultiplier, this.retryCount), this.config.maxDelay);
|
|
78
|
+
this.retryCount++;
|
|
79
|
+
this.log(`将在 ${delay}ms 后进行第 ${this.retryCount} 次重连尝试...`);
|
|
80
|
+
this.reconnectTimer = setTimeout(() => {
|
|
81
|
+
this.reconnectTimer = null;
|
|
82
|
+
this.connect();
|
|
83
|
+
}, delay);
|
|
84
|
+
}
|
|
85
|
+
send(data) {
|
|
86
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
87
|
+
this.ws.send(JSON.stringify(data));
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
this.log('WebSocket 未连接,无法发送消息');
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
onMessage(handler) {
|
|
94
|
+
this.messageHandlers.push(handler);
|
|
95
|
+
}
|
|
96
|
+
onOpen(handler) {
|
|
97
|
+
this.openHandlers.push(handler);
|
|
98
|
+
}
|
|
99
|
+
onClose(handler) {
|
|
100
|
+
this.closeHandlers.push(handler);
|
|
101
|
+
}
|
|
102
|
+
onError(handler) {
|
|
103
|
+
this.errorHandlers.push(handler);
|
|
104
|
+
}
|
|
105
|
+
close() {
|
|
106
|
+
this.isManualClose = true;
|
|
107
|
+
if (this.reconnectTimer) {
|
|
108
|
+
clearTimeout(this.reconnectTimer);
|
|
109
|
+
this.reconnectTimer = null;
|
|
110
|
+
}
|
|
111
|
+
if (this.ws) {
|
|
112
|
+
this.ws.close();
|
|
113
|
+
this.ws = null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
getReadyState() {
|
|
117
|
+
return this.ws?.readyState ?? WebSocket.CLOSED;
|
|
118
|
+
}
|
|
119
|
+
getRetryCount() {
|
|
120
|
+
return this.retryCount;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// const ws = new ReconnectingWebSocket('ws://localhost:51516/livecode/ws?id=test-live-app', {
|
|
124
|
+
// maxRetries: Infinity, // 无限重试
|
|
125
|
+
// retryDelay: 1000, // 初始重试延迟 1 秒
|
|
126
|
+
// maxDelay: 30000, // 最大延迟 30 秒
|
|
127
|
+
// backoffMultiplier: 2, // 指数退避倍数
|
|
128
|
+
// });
|
|
129
|
+
|
|
130
|
+
const handleCallWsApp = async (ws, app, message) => {
|
|
131
|
+
return handleCallApp((data) => {
|
|
132
|
+
ws.send(data);
|
|
133
|
+
}, app, message);
|
|
134
|
+
};
|
|
135
|
+
const handleCallApp = async (send, app, message) => {
|
|
136
|
+
if (message.type === 'router' && message.id) {
|
|
137
|
+
const data = message?.data;
|
|
138
|
+
if (!message.id) {
|
|
139
|
+
console.error('Message id is required for router type');
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (!data) {
|
|
143
|
+
send({
|
|
144
|
+
type: 'router',
|
|
145
|
+
id: message.id,
|
|
146
|
+
data: { code: 500, message: 'No data received' }
|
|
147
|
+
});
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const { tokenUser, ...rest } = data || {};
|
|
151
|
+
const res = await app.run(rest, {
|
|
152
|
+
state: { tokenUser },
|
|
153
|
+
appId: app.appId,
|
|
154
|
+
});
|
|
155
|
+
send({
|
|
156
|
+
type: 'router',
|
|
157
|
+
id: message.id,
|
|
158
|
+
data: res
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
class Ws {
|
|
163
|
+
wsClient;
|
|
164
|
+
app;
|
|
165
|
+
showLog = true;
|
|
166
|
+
constructor(opts) {
|
|
167
|
+
const { url, app, showLog = true, handleMessage = handleCallWsApp, ...rest } = opts;
|
|
168
|
+
this.wsClient = new ReconnectingWebSocket(url, rest);
|
|
169
|
+
this.app = app;
|
|
170
|
+
this.showLog = showLog;
|
|
171
|
+
this.wsClient.connect();
|
|
172
|
+
const onMessage = async (data) => {
|
|
173
|
+
return handleMessage(this.wsClient, this.app, data);
|
|
174
|
+
};
|
|
175
|
+
this.wsClient.onMessage(onMessage);
|
|
176
|
+
}
|
|
177
|
+
send(data) {
|
|
178
|
+
return this.wsClient.send(data);
|
|
179
|
+
}
|
|
180
|
+
log(...args) {
|
|
181
|
+
if (this.showLog)
|
|
182
|
+
console.log('[Ws]', ...args);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export { ReconnectingWebSocket, Ws, handleCallApp, handleCallWsApp };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package",
|
|
3
3
|
"name": "@kevisual/router",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.68",
|
|
5
5
|
"description": "",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./dist/router.js",
|
|
@@ -24,17 +24,18 @@
|
|
|
24
24
|
"packageManager": "pnpm@10.28.2",
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"@kevisual/context": "^0.0.4",
|
|
27
|
+
"@kevisual/dts": "^0.0.3",
|
|
27
28
|
"@kevisual/js-filter": "^0.0.5",
|
|
28
29
|
"@kevisual/local-proxy": "^0.0.8",
|
|
29
|
-
"@kevisual/query": "^0.0.
|
|
30
|
-
"@kevisual/use-config": "^1.0.
|
|
31
|
-
"@opencode-ai/plugin": "^1.1.
|
|
30
|
+
"@kevisual/query": "^0.0.39",
|
|
31
|
+
"@kevisual/use-config": "^1.0.30",
|
|
32
|
+
"@opencode-ai/plugin": "^1.1.48",
|
|
32
33
|
"@rollup/plugin-alias": "^6.0.0",
|
|
33
34
|
"@rollup/plugin-commonjs": "29.0.0",
|
|
34
35
|
"@rollup/plugin-node-resolve": "^16.0.3",
|
|
35
36
|
"@rollup/plugin-typescript": "^12.3.0",
|
|
36
37
|
"@types/bun": "^1.3.8",
|
|
37
|
-
"@types/node": "^25.
|
|
38
|
+
"@types/node": "^25.2.0",
|
|
38
39
|
"@types/send": "^1.2.1",
|
|
39
40
|
"@types/ws": "^8.18.1",
|
|
40
41
|
"@types/xml2js": "^0.4.14",
|
|
@@ -52,16 +53,14 @@
|
|
|
52
53
|
"typescript": "^5.9.3",
|
|
53
54
|
"ws": "npm:@kevisual/ws",
|
|
54
55
|
"xml2js": "^0.6.2",
|
|
55
|
-
"zod": "^4.3.6"
|
|
56
|
+
"zod": "^4.3.6",
|
|
57
|
+
"hono": "^4.11.7"
|
|
56
58
|
},
|
|
57
59
|
"repository": {
|
|
58
60
|
"type": "git",
|
|
59
61
|
"url": "git+https://github.com/abearxiong/kevisual-router.git"
|
|
60
62
|
},
|
|
61
|
-
"dependencies": {
|
|
62
|
-
"@kevisual/dts": "^0.0.3",
|
|
63
|
-
"hono": "^4.11.7"
|
|
64
|
-
},
|
|
63
|
+
"dependencies": {},
|
|
65
64
|
"publishConfig": {
|
|
66
65
|
"access": "public"
|
|
67
66
|
},
|
|
@@ -88,6 +87,7 @@
|
|
|
88
87
|
"require": "./dist/router-define.js",
|
|
89
88
|
"types": "./dist/router-define.d.ts"
|
|
90
89
|
},
|
|
90
|
+
"./ws": "./dist/ws.js",
|
|
91
91
|
"./mod.ts": {
|
|
92
92
|
"import": "./mod.ts",
|
|
93
93
|
"require": "./mod.ts",
|
|
@@ -102,4 +102,4 @@
|
|
|
102
102
|
"require": "./src/modules/*"
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
|
-
}
|
|
105
|
+
}
|
package/src/browser.ts
CHANGED
|
@@ -8,7 +8,7 @@ export type { RouteContext, RouteOpts, RouteMiddleware } from './route.ts';
|
|
|
8
8
|
|
|
9
9
|
export type { Run, Skill } from './route.ts';
|
|
10
10
|
|
|
11
|
-
export { createSkill, tool } from './route.ts';
|
|
11
|
+
export { createSkill, tool, fromJSONSchema, toJSONSchema } from './route.ts';
|
|
12
12
|
|
|
13
13
|
export { CustomError } from './result/error.ts';
|
|
14
14
|
|
package/src/index.ts
CHANGED
|
@@ -8,7 +8,7 @@ export type { RouteContext, RouteOpts, RouteMiddleware } from './route.ts';
|
|
|
8
8
|
|
|
9
9
|
export type { Run, Skill } from './route.ts';
|
|
10
10
|
|
|
11
|
-
export { createSkill, tool } from './route.ts';
|
|
11
|
+
export { createSkill, tool, fromJSONSchema, toJSONSchema } from './route.ts';
|
|
12
12
|
|
|
13
13
|
export { CustomError } from './result/error.ts';
|
|
14
14
|
|
package/src/route.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { nanoid } from 'nanoid';
|
|
2
2
|
import { CustomError } from './result/error.ts';
|
|
3
3
|
import { pick } from './utils/pick.ts';
|
|
4
|
-
import { listenProcess } from './utils/listen-process.ts';
|
|
4
|
+
import { listenProcess, MockProcess } from './utils/listen-process.ts';
|
|
5
5
|
import { z } from 'zod';
|
|
6
6
|
import { filter } from '@kevisual/js-filter'
|
|
7
7
|
|
|
@@ -243,11 +243,42 @@ export class Route<U = { [key: string]: any }, T extends SimpleObject = SimpleOb
|
|
|
243
243
|
throw new CustomError(...args);
|
|
244
244
|
}
|
|
245
245
|
}
|
|
246
|
+
export const toJSONSchema = (route: RouteInfo) => {
|
|
247
|
+
const pickValues = pick(route, pickValue as any);
|
|
248
|
+
if (pickValues?.metadata?.args) {
|
|
249
|
+
const args = pickValues.metadata.args;
|
|
250
|
+
const keys = Object.keys(args);
|
|
251
|
+
const newArgs: { [key: string]: any } = {};
|
|
252
|
+
for (let key of keys) {
|
|
253
|
+
const item = args[key] as z.ZodAny;
|
|
254
|
+
if (item && typeof item === 'object' && typeof item.toJSONSchema === 'function') {
|
|
255
|
+
newArgs[key] = item.toJSONSchema();
|
|
256
|
+
} else {
|
|
257
|
+
newArgs[key] = args[key]; // 可能不是schema
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
pickValues.metadata.args = newArgs;
|
|
261
|
+
}
|
|
262
|
+
return pickValues;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export const fromJSONSchema = (route: RouteInfo): {
|
|
266
|
+
[key: string]: z.ZodTypeAny
|
|
267
|
+
} => {
|
|
268
|
+
const args = route?.metadata?.args || {};
|
|
269
|
+
const keys = Object.keys(args);
|
|
270
|
+
const newArgs: { [key: string]: any } = {};
|
|
271
|
+
for (let key of keys) {
|
|
272
|
+
const item = args[key];
|
|
273
|
+
newArgs[key] = z.fromJSONSchema(item);
|
|
274
|
+
}
|
|
275
|
+
return newArgs;
|
|
276
|
+
}
|
|
246
277
|
|
|
247
278
|
/**
|
|
248
|
-
* @parmas
|
|
279
|
+
* @parmas overwrite 是否覆盖已存在的route,默认true
|
|
249
280
|
*/
|
|
250
|
-
export type AddOpts = {
|
|
281
|
+
export type AddOpts = { overwrite?: boolean };
|
|
251
282
|
export class QueryRouter {
|
|
252
283
|
appId: string = '';
|
|
253
284
|
routes: Route[];
|
|
@@ -262,14 +293,14 @@ export class QueryRouter {
|
|
|
262
293
|
* @param opts
|
|
263
294
|
*/
|
|
264
295
|
add(route: Route, opts?: AddOpts) {
|
|
265
|
-
const
|
|
296
|
+
const overwrite = opts?.overwrite ?? true;
|
|
266
297
|
const has = this.routes.findIndex((r) => r.path === route.path && r.key === route.key);
|
|
267
298
|
|
|
268
299
|
if (has !== -1) {
|
|
269
|
-
if (!
|
|
300
|
+
if (!overwrite) {
|
|
270
301
|
return;
|
|
271
302
|
}
|
|
272
|
-
// 如果存在,且
|
|
303
|
+
// 如果存在,且overwrite为true,则覆盖
|
|
273
304
|
this.routes.splice(has, 1);
|
|
274
305
|
}
|
|
275
306
|
this.routes.push(route);
|
|
@@ -555,7 +586,23 @@ export class QueryRouter {
|
|
|
555
586
|
}
|
|
556
587
|
getList(filter?: (route: Route) => boolean): RouteInfo[] {
|
|
557
588
|
return this.routes.filter(filter || (() => true)).map((r) => {
|
|
558
|
-
|
|
589
|
+
const pickValues = pick(r, pickValue as any);
|
|
590
|
+
if (pickValues?.metadata?.args) {
|
|
591
|
+
// const demoArgs = { k: tool.schema.string().describe('示例参数') };
|
|
592
|
+
const args = pickValues.metadata.args;
|
|
593
|
+
const keys = Object.keys(args);
|
|
594
|
+
const newArgs: { [key: string]: any } = {};
|
|
595
|
+
for (let key of keys) {
|
|
596
|
+
const item = args[key] as z.ZodAny;
|
|
597
|
+
if (item && typeof item === 'object' && typeof item.toJSONSchema === 'function') {
|
|
598
|
+
newArgs[key] = item.toJSONSchema();
|
|
599
|
+
} else {
|
|
600
|
+
newArgs[key] = args[key]; // 可能不是schema
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
pickValues.metadata.args = newArgs;
|
|
604
|
+
}
|
|
605
|
+
return pickValues;
|
|
559
606
|
});
|
|
560
607
|
}
|
|
561
608
|
/**
|
|
@@ -634,7 +681,7 @@ export class QueryRouter {
|
|
|
634
681
|
* -- .send
|
|
635
682
|
*/
|
|
636
683
|
wait(params?: { path?: string; key?: string; payload?: any }, opts?: {
|
|
637
|
-
|
|
684
|
+
mockProcess?: MockProcess,
|
|
638
685
|
timeout?: number,
|
|
639
686
|
getList?: boolean
|
|
640
687
|
force?: boolean
|
|
@@ -646,6 +693,8 @@ export class QueryRouter {
|
|
|
646
693
|
}
|
|
647
694
|
return listenProcess({ app: this as any, params, ...opts });
|
|
648
695
|
}
|
|
696
|
+
static toJSONSchema = toJSONSchema;
|
|
697
|
+
static fromJSONSchema = fromJSONSchema;
|
|
649
698
|
}
|
|
650
699
|
|
|
651
700
|
type QueryRouterServerOpts = {
|
|
@@ -729,4 +778,5 @@ export class QueryRouterServer extends QueryRouter {
|
|
|
729
778
|
}
|
|
730
779
|
|
|
731
780
|
|
|
732
|
-
export class Mini extends QueryRouterServer { }
|
|
781
|
+
export class Mini extends QueryRouterServer { }
|
|
782
|
+
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import WebSocket from 'ws';
|
|
2
|
+
|
|
3
|
+
export type ReconnectConfig = {
|
|
4
|
+
/**
|
|
5
|
+
* 重连配置选项, 最大重试次数,默认无限
|
|
6
|
+
*/
|
|
7
|
+
maxRetries?: number;
|
|
8
|
+
/**
|
|
9
|
+
* 重连配置选项, 重试延迟(ms),默认1000
|
|
10
|
+
*/
|
|
11
|
+
retryDelay?: number;
|
|
12
|
+
/**
|
|
13
|
+
* 重连配置选项, 最大延迟(ms),默认30000
|
|
14
|
+
*/
|
|
15
|
+
maxDelay?: number;
|
|
16
|
+
/**
|
|
17
|
+
* 重连配置选项, 退避倍数,默认2
|
|
18
|
+
*/
|
|
19
|
+
backoffMultiplier?: number;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 一个支持自动重连的 WebSocket 客户端。
|
|
24
|
+
* 在连接断开时会根据配置进行重连尝试,支持指数退避。
|
|
25
|
+
*/
|
|
26
|
+
export class ReconnectingWebSocket {
|
|
27
|
+
private ws: WebSocket | null = null;
|
|
28
|
+
private url: string;
|
|
29
|
+
private config: Required<ReconnectConfig>;
|
|
30
|
+
private retryCount: number = 0;
|
|
31
|
+
private reconnectTimer: NodeJS.Timeout | null = null;
|
|
32
|
+
private isManualClose: boolean = false;
|
|
33
|
+
private messageHandlers: Array<(data: any) => void> = [];
|
|
34
|
+
private openHandlers: Array<() => void> = [];
|
|
35
|
+
private closeHandlers: Array<(code: number, reason: Buffer) => void> = [];
|
|
36
|
+
private errorHandlers: Array<(error: Error) => void> = [];
|
|
37
|
+
|
|
38
|
+
constructor(url: string, config: ReconnectConfig = {}) {
|
|
39
|
+
this.url = url;
|
|
40
|
+
this.config = {
|
|
41
|
+
maxRetries: config.maxRetries ?? Infinity,
|
|
42
|
+
retryDelay: config.retryDelay ?? 1000,
|
|
43
|
+
maxDelay: config.maxDelay ?? 30000,
|
|
44
|
+
backoffMultiplier: config.backoffMultiplier ?? 2,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
log(...args: any[]): void {
|
|
48
|
+
console.log('[ReconnectingWebSocket]', ...args);
|
|
49
|
+
}
|
|
50
|
+
error(...args: any[]): void {
|
|
51
|
+
console.error('[ReconnectingWebSocket]', ...args);
|
|
52
|
+
}
|
|
53
|
+
connect(): void {
|
|
54
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
this.log(`正在连接到 ${this.url}...`);
|
|
59
|
+
this.ws = new WebSocket(this.url);
|
|
60
|
+
|
|
61
|
+
this.ws.on('open', () => {
|
|
62
|
+
this.log('WebSocket 连接已打开');
|
|
63
|
+
this.retryCount = 0;
|
|
64
|
+
this.openHandlers.forEach(handler => handler());
|
|
65
|
+
this.send({ type: 'heartbeat', timestamp: new Date().toISOString() });
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
this.ws.on('message', (data: any) => {
|
|
69
|
+
this.messageHandlers.forEach(handler => {
|
|
70
|
+
try {
|
|
71
|
+
const message = JSON.parse(data.toString());
|
|
72
|
+
handler(message);
|
|
73
|
+
} catch {
|
|
74
|
+
handler(data.toString());
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
this.ws.on('close', (code: number, reason: Buffer) => {
|
|
80
|
+
this.log(`WebSocket 连接已关闭: code=${code}, reason=${reason.toString()}`);
|
|
81
|
+
this.closeHandlers.forEach(handler => handler(code, reason));
|
|
82
|
+
|
|
83
|
+
if (!this.isManualClose) {
|
|
84
|
+
this.scheduleReconnect();
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
this.ws.on('error', (error: Error) => {
|
|
89
|
+
this.error('WebSocket 错误:', error.message);
|
|
90
|
+
this.errorHandlers.forEach(handler => handler(error));
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private scheduleReconnect(): void {
|
|
95
|
+
if (this.reconnectTimer) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (this.retryCount >= this.config.maxRetries) {
|
|
100
|
+
this.error(`已达到最大重试次数 (${this.config.maxRetries}),停止重连`);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 计算延迟(指数退避)
|
|
105
|
+
const delay = Math.min(
|
|
106
|
+
this.config.retryDelay * Math.pow(this.config.backoffMultiplier, this.retryCount),
|
|
107
|
+
this.config.maxDelay
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
this.retryCount++;
|
|
111
|
+
this.log(`将在 ${delay}ms 后进行第 ${this.retryCount} 次重连尝试...`);
|
|
112
|
+
|
|
113
|
+
this.reconnectTimer = setTimeout(() => {
|
|
114
|
+
this.reconnectTimer = null;
|
|
115
|
+
this.connect();
|
|
116
|
+
}, delay);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
send(data: any): boolean {
|
|
120
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
121
|
+
this.ws.send(JSON.stringify(data));
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
this.log('WebSocket 未连接,无法发送消息');
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
onMessage(handler: (data: any) => void): void {
|
|
129
|
+
this.messageHandlers.push(handler);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
onOpen(handler: () => void): void {
|
|
133
|
+
this.openHandlers.push(handler);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
onClose(handler: (code: number, reason: Buffer) => void): void {
|
|
137
|
+
this.closeHandlers.push(handler);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
onError(handler: (error: Error) => void): void {
|
|
141
|
+
this.errorHandlers.push(handler);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
close(): void {
|
|
145
|
+
this.isManualClose = true;
|
|
146
|
+
if (this.reconnectTimer) {
|
|
147
|
+
clearTimeout(this.reconnectTimer);
|
|
148
|
+
this.reconnectTimer = null;
|
|
149
|
+
}
|
|
150
|
+
if (this.ws) {
|
|
151
|
+
this.ws.close();
|
|
152
|
+
this.ws = null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
getReadyState(): number {
|
|
157
|
+
return this.ws?.readyState ?? WebSocket.CLOSED;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
getRetryCount(): number {
|
|
161
|
+
return this.retryCount;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// const ws = new ReconnectingWebSocket('ws://localhost:51516/livecode/ws?id=test-live-app', {
|
|
166
|
+
// maxRetries: Infinity, // 无限重试
|
|
167
|
+
// retryDelay: 1000, // 初始重试延迟 1 秒
|
|
168
|
+
// maxDelay: 30000, // 最大延迟 30 秒
|
|
169
|
+
// backoffMultiplier: 2, // 指数退避倍数
|
|
170
|
+
// });
|
package/src/ws.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { ReconnectingWebSocket, ReconnectConfig } from "./server/reconnect-ws.ts";
|
|
2
|
+
|
|
3
|
+
export * from "./server/reconnect-ws.ts";
|
|
4
|
+
import type { App } from "./app.ts";
|
|
5
|
+
|
|
6
|
+
export const handleCallWsApp = async (ws: ReconnectingWebSocket, app: App, message: any) => {
|
|
7
|
+
return handleCallApp((data: any) => {
|
|
8
|
+
ws.send(data);
|
|
9
|
+
}, app, message);
|
|
10
|
+
}
|
|
11
|
+
export const handleCallApp = async (send: (data: any) => void, app: App, message: any) => {
|
|
12
|
+
if (message.type === 'router' && message.id) {
|
|
13
|
+
const data = message?.data;
|
|
14
|
+
if (!message.id) {
|
|
15
|
+
console.error('Message id is required for router type');
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (!data) {
|
|
19
|
+
send({
|
|
20
|
+
type: 'router',
|
|
21
|
+
id: message.id,
|
|
22
|
+
data: { code: 500, message: 'No data received' }
|
|
23
|
+
});
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const { tokenUser, ...rest } = data || {};
|
|
27
|
+
const res = await app.run(rest, {
|
|
28
|
+
state: { tokenUser },
|
|
29
|
+
appId: app.appId,
|
|
30
|
+
});
|
|
31
|
+
send({
|
|
32
|
+
type: 'router',
|
|
33
|
+
id: message.id,
|
|
34
|
+
data: res
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export class Ws {
|
|
39
|
+
wsClient: ReconnectingWebSocket;
|
|
40
|
+
app: App;
|
|
41
|
+
showLog: boolean = true;
|
|
42
|
+
constructor(opts?: ReconnectConfig & {
|
|
43
|
+
url: string;
|
|
44
|
+
app: App;
|
|
45
|
+
showLog?: boolean;
|
|
46
|
+
handleMessage?: (ws: ReconnectingWebSocket, app: App, message: any) => void;
|
|
47
|
+
}) {
|
|
48
|
+
const { url, app, showLog = true, handleMessage = handleCallWsApp, ...rest } = opts;
|
|
49
|
+
this.wsClient = new ReconnectingWebSocket(url, rest);
|
|
50
|
+
this.app = app;
|
|
51
|
+
this.showLog = showLog;
|
|
52
|
+
this.wsClient.connect();
|
|
53
|
+
const onMessage = async (data: any) => {
|
|
54
|
+
return handleMessage(this.wsClient, this.app, data);
|
|
55
|
+
}
|
|
56
|
+
this.wsClient.onMessage(onMessage);
|
|
57
|
+
}
|
|
58
|
+
send(data: any): boolean {
|
|
59
|
+
return this.wsClient.send(data);
|
|
60
|
+
}
|
|
61
|
+
log(...args: any[]): void {
|
|
62
|
+
if (this.showLog)
|
|
63
|
+
console.log('[Ws]', ...args);
|
|
64
|
+
}
|
|
65
|
+
}
|