xtraderClient/sdk/userSocket.ts
2026-02-26 19:29:20 +08:00

229 lines
7.9 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 前端 User WebSocket SDK浏览器使用原生 WebSocketNode 测试需注入 global.WebSocket
// 消息类型定义
export type UserUpdateType = 'order_update' | 'position_update' | 'balance_update' | 'welcome';
export interface UserMsg {
type: UserUpdateType;
data?: any;
msg?: string; // mainly for welcome
timestamp?: number;
t?: number; // for welcome timestamp
}
// 订单数据结构 (对应后端 ClobOrder)
export interface OrderData {
ID: number;
userID: number;
market: string;
status: number;
assetID: string;
side: number;
price: number;
originalSize: number;
sizeMatched: number;
outcome: string;
expiration: number;
orderType: number;
feeRateBps: number;
CreatedAt?: string;
UpdatedAt?: string;
}
// 持仓数据结构 (对应后端 ClobPosition)
export interface PositionData {
ID: number;
userID: number;
marketID: string;
tokenId: string;
side: number;
size: number;
available: number;
}
// 余额数据结构 (对应后端 PmTokenBalance)
export interface BalanceData {
ID: string;
userID: number;
amount: string;
available: string;
frozen: string;
tokenType: string;
token_id: string; // 注意后端字段可能是不一致的,这里根据 Go 结构体推测,或者是 json tag
market_id: string;
wallet_address: string;
}
// SDK 配置选项
export interface UserSocketOptions {
url?: string;
token: string; // 必须提供 Token
autoReconnect?: boolean;
reconnectInterval?: number;
maxReconnectAttempts?: number;
}
// 回调函数类型(兼容浏览器 Event/CloseEvent
export type ConnectCallback = (event: Event) => void;
export type DisconnectCallback = (event: CloseEvent) => void;
export type ErrorCallback = (event: Event) => void;
export type WelcomeCallback = (msg: UserMsg) => void;
export type OrderCallback = (data: OrderData) => void;
export type PositionCallback = (data: PositionData) => void;
export type BalanceCallback = (data: BalanceData) => void;
export class UserSdk {
private url: string;
private token: string;
private ws: InstanceType<typeof WebSocket> | null = null;
private autoReconnect: boolean;
private reconnectInterval: number;
private maxReconnectAttempts: number;
private reconnectAttempts = 0;
private isExplicitClose = false;
private listeners = {
connect: [] as ConnectCallback[],
disconnect: [] as DisconnectCallback[],
error: [] as ErrorCallback[],
welcome: [] as WelcomeCallback[],
order: [] as OrderCallback[],
position: [] as PositionCallback[],
balance: [] as BalanceCallback[]
};
constructor(options: UserSocketOptions) {
this.token = options.token;
this.autoReconnect = options.autoReconnect ?? true;
this.reconnectInterval = options.reconnectInterval ?? 3000;
this.maxReconnectAttempts = options.maxReconnectAttempts ?? 5;
// 构造 URL
let baseUrl = options.url || "ws://localhost:8888/clob/ws/user";
if (baseUrl.startsWith("/")) {
// 如果是相对路径 (浏览器环境),自动补充 host
if (typeof window !== 'undefined' && window.location) {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
baseUrl = `${protocol}//${window.location.host}${baseUrl}`;
} else {
baseUrl = "ws://localhost:8888" + baseUrl;
}
}
// 拼接 Token
const separator = baseUrl.includes('?') ? '&' : '?';
this.url = `${baseUrl}${separator}token=${this.token}`;
console.log('[UserSdk] 初始化:', {
url: this.url.replace(/token=[^&]+/, 'token=***'),
autoReconnect: this.autoReconnect,
reconnectInterval: this.reconnectInterval,
maxReconnectAttempts: this.maxReconnectAttempts,
});
}
// 连接
public connect() {
if (this.ws) {
console.warn('[UserSdk] 已连接或正在连接,跳过');
return;
}
try {
console.log('[UserSdk] 正在连接...', this.url.replace(/token=[^&]+/, 'token=***'));
this.ws = new WebSocket(this.url);
this.ws.onopen = (event: Event) => {
console.log('[UserSdk] 已连接');
this.reconnectAttempts = 0;
this.listeners.connect.forEach(cb => cb(event));
};
this.ws.onclose = (event: CloseEvent) => {
console.log('[UserSdk] 已断开', { code: event.code, reason: event.reason, wasClean: event.wasClean });
this.ws = null;
this.listeners.disconnect.forEach(cb => cb(event));
if (this.autoReconnect && !this.isExplicitClose) {
this.handleReconnect();
}
};
this.ws.onerror = (event: Event) => {
console.error('[UserSdk] WebSocket 错误', event);
this.listeners.error.forEach(cb => cb(event));
};
this.ws.onmessage = (event: MessageEvent) => {
try {
const raw = typeof event.data === 'string' ? event.data : String(event.data);
const msg = JSON.parse(raw) as UserMsg;
this.handleMessage(msg);
} catch (e) {
console.error('[UserSdk] 消息解析失败', event.data, e);
}
};
} catch (e) {
console.error('[UserSdk] 连接失败', e);
if (this.autoReconnect) this.handleReconnect();
}
}
// 断开连接
public disconnect() {
console.log('[UserSdk] 主动断开连接');
this.isExplicitClose = true;
if (this.ws) {
this.ws.close();
this.ws = null;
}
}
// 消息分发
private handleMessage(msg: UserMsg) {
switch (msg.type) {
case 'welcome':
console.log('[UserSdk] 收到 welcome', msg.msg ?? msg);
this.listeners.welcome.forEach(cb => cb(msg));
break;
case 'order_update':
console.log('[UserSdk] 收到 order_update', msg.data);
this.listeners.order.forEach(cb => cb(msg.data));
break;
case 'position_update':
console.log('[UserSdk] 收到 position_update', msg.data);
this.listeners.position.forEach(cb => cb(msg.data));
break;
case 'balance_update':
console.log('[UserSdk] 收到 balance_update', msg.data);
this.listeners.balance.forEach(cb => cb(msg.data));
break;
default:
console.warn('[UserSdk] 未知消息类型', msg);
}
}
// 重连逻辑
private handleReconnect() {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.error('[UserSdk] 已达最大重连次数', this.maxReconnectAttempts);
return;
}
this.reconnectAttempts++;
console.log('[UserSdk] 重连中...', { attempt: this.reconnectAttempts, max: this.maxReconnectAttempts, delayMs: this.reconnectInterval });
setTimeout(() => {
this.connect();
}, this.reconnectInterval);
}
// 事件注册方法
public onConnect(cb: ConnectCallback) { this.listeners.connect.push(cb); }
public onDisconnect(cb: DisconnectCallback) { this.listeners.disconnect.push(cb); }
public onError(cb: ErrorCallback) { this.listeners.error.push(cb); }
public onWelcome(cb: WelcomeCallback) { this.listeners.welcome.push(cb); }
public onOrderUpdate(cb: OrderCallback) { this.listeners.order.push(cb); }
public onPositionUpdate(cb: PositionCallback) { this.listeners.position.push(cb); }
public onBalanceUpdate(cb: BalanceCallback) { this.listeners.balance.push(cb); }
}