217 lines
7.2 KiB
TypeScript
217 lines
7.2 KiB
TypeScript
// 前端 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<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); }
|
||
}
|