feat: Implement LiveCode module with WebSocket and SSE support
- Added config management using `useConfig` for environment variables. - Created `LiveCode` class to manage WebSocket connections and routing. - Implemented `SSEManager` for Server-Sent Events handling. - Developed `WSSManager` for managing WebSocket connections with heartbeat functionality. - Introduced `ReconnectingWebSocket` class for robust WebSocket client with automatic reconnection. - Added test files for live application demonstrating WebSocket and TCP server integration.
This commit is contained in:
131
assistant/src/module/livecode/index.ts
Normal file
131
assistant/src/module/livecode/index.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import { WSSManager } from './wss.ts';
|
||||
import { App, Route } from '@kevisual/router'
|
||||
import { WebSocketReq } from '@kevisual/router'
|
||||
import { EventEmitter } from 'eventemitter3';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
|
||||
const letter = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
const customId = customAlphabet(letter, 16);
|
||||
export class LiveCode {
|
||||
wssManager: WSSManager;
|
||||
app: App;
|
||||
emitter: EventEmitter;
|
||||
constructor(app: App) {
|
||||
this.wssManager = new WSSManager({ heartbeatInterval: 5000 });
|
||||
this.app = app;
|
||||
this.emitter = new EventEmitter();
|
||||
console.log('[LiveCode] 模块已初始化');
|
||||
}
|
||||
async conn(req: WebSocketReq) {
|
||||
const { ws, emitter, id } = req;
|
||||
const that = this;
|
||||
// @ts-ignore
|
||||
let wid = ws.data?.wid;
|
||||
if (!wid) {
|
||||
const _id = this.wssManager.addConnection(req, { userId: id });
|
||||
// @ts-ignore
|
||||
ws.data.wid = _id;
|
||||
emitter.once('close--' + id, () => {
|
||||
that.wssManager.closeConnection(_id);
|
||||
this.deinitAppRoutes(_id);
|
||||
});
|
||||
console.log('[LiveCode]新的 WebSocket 连接已打开', _id);
|
||||
const res = await that.init(_id);
|
||||
if (res.code === 200) {
|
||||
console.log('[LiveCode]初始化路由列表完成');
|
||||
that.initAppRoutes(res.data?.list || [], _id);
|
||||
} else {
|
||||
console.error('[LiveCode]初始化路由列表失败:', res?.message);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
that.onMessage(req);
|
||||
return this;
|
||||
}
|
||||
getWss(id: string) {
|
||||
return this.wssManager.getConnection(id)
|
||||
}
|
||||
async init(id: string): Promise<{ code: number, message?: string, data?: any }> {
|
||||
return this.sendData({ path: 'router', key: 'list', }, id);
|
||||
}
|
||||
sendData(data: any, id: string): Promise<{ code: number, message?: string, data?: any }> {
|
||||
const reqId = customId()
|
||||
const wss = this.getWss(id);
|
||||
if (!wss) {
|
||||
return Promise.resolve({ code: 500, message: '连接不存在或已关闭' });
|
||||
}
|
||||
const emitter = this.emitter;
|
||||
const wsReq = wss.wsReq;
|
||||
try {
|
||||
wsReq.ws.send(JSON.stringify({
|
||||
type: 'router',
|
||||
id: reqId,
|
||||
data: data
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('[LiveCode]发送数据失败:', error);
|
||||
return Promise.resolve({ code: 500, message: '发送数据失败' });
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
const timeout = setTimeout(() => {
|
||||
resolve({ code: 500, message: '请求超时' });
|
||||
emitter.off(reqId, listenOnce);
|
||||
}, 5000);
|
||||
const listenOnce = (resData: any) => {
|
||||
clearTimeout(timeout);
|
||||
resolve(resData);
|
||||
emitter.off(reqId, listenOnce);
|
||||
}
|
||||
emitter.once(reqId, listenOnce);
|
||||
});
|
||||
}
|
||||
onMessage(req: WebSocketReq) {
|
||||
const { data } = req;
|
||||
if (data?.id) {
|
||||
// console.log('LiveCode 收到消息:', data);
|
||||
this.emitter.emit(data.id, data.data);
|
||||
} else {
|
||||
console.warn('[LiveCode] 未知的消息格式', data);
|
||||
}
|
||||
}
|
||||
initAppRoutes(list: Route[], wid: string) {
|
||||
for (const route of list) {
|
||||
const path = route.path || '';
|
||||
const id = route.id || '';
|
||||
if (path.startsWith('router') || path.startsWith('auth') || path.startsWith('admin-autu') || path.startsWith('call')) {
|
||||
continue;
|
||||
}
|
||||
// console.log('注册路由:', route.path, route.description, route.metadata, route.id);
|
||||
this.app.route({
|
||||
path: route.id,
|
||||
key: route.key,
|
||||
description: route.description,
|
||||
metadata: {
|
||||
...route.metadata,
|
||||
liveCodeId: wid
|
||||
},
|
||||
middleware: ['auth'],
|
||||
}).define(async (ctx) => {
|
||||
const { token, cookie, ...rest } = ctx.query;
|
||||
const tokenUser = ctx.state.tokernUser;
|
||||
const res = await this.sendData({
|
||||
id: route.id,
|
||||
tokenUser,
|
||||
payload: rest,
|
||||
}, wid);
|
||||
// console.log('路由响应数据:', res);
|
||||
ctx.forward(res)
|
||||
}).addTo(this.app, {
|
||||
// override: false,
|
||||
// // @ts-ignore
|
||||
// overwrite: false
|
||||
});
|
||||
}
|
||||
}
|
||||
deinitAppRoutes(wid: string) {
|
||||
const routesToRemove = this.app.routes.filter(route => route.metadata?.liveCodeId === wid);
|
||||
for (const route of routesToRemove) {
|
||||
this.app.removeById(route.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user