// 前端 User WebSocket SDK(浏览器使用原生 WebSocket,Node 测试需注入 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 | 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); } }