xtraderClient/sdk/userSocket.ts
2026-02-26 14:44:36 +08:00

217 lines
7.2 KiB
TypeScript
Raw 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}`;
}
// 连接
public connect() {
if (this.ws) {
console.warn("UserSdk: WebSocket already connected or connecting");
return;
}
try {
console.log(`UserSdk: Connecting to ${this.url}...`);
this.ws = new WebSocket(this.url);
this.ws.onopen = (event: Event) => {
console.log("UserSdk: Connected");
this.reconnectAttempts = 0;
this.listeners.connect.forEach(cb => cb(event));
};
this.ws.onclose = (event: CloseEvent) => {
console.log(`UserSdk: Disconnected (Code: ${event.code})`);
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: Error", 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: Failed to parse message", event.data);
}
};
} catch (e) {
console.error("UserSdk: Connection failed", e);
if (this.autoReconnect) this.handleReconnect();
}
}
// 断开连接
public disconnect() {
this.isExplicitClose = true;
if (this.ws) {
this.ws.close();
this.ws = null;
}
}
// 消息分发
private handleMessage(msg: UserMsg) {
switch (msg.type) {
case 'welcome':
this.listeners.welcome.forEach(cb => cb(msg));
break;
case 'order_update':
this.listeners.order.forEach(cb => cb(msg.data));
break;
case 'position_update':
this.listeners.position.forEach(cb => cb(msg.data));
break;
case 'balance_update':
this.listeners.balance.forEach(cb => cb(msg.data));
break;
default:
console.warn("UserSdk: Unknown message type", msg);
}
}
// 重连逻辑
private handleReconnect() {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.error("UserSdk: Max reconnect attempts reached");
return;
}
this.reconnectAttempts++;
console.log(`UserSdk: Reconnecting in ${this.reconnectInterval}ms (Attempt ${this.reconnectAttempts})...`);
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); }
}