openchain-sdk-yxl-ts
Version:
OpenChain SDK for browser
161 lines (135 loc) • 4.69 kB
JavaScript
import { EventEmitter } from 'events';
import protobuf from 'protobufjs';
import { Buffer } from 'buffer';
import WebSocket from './browser-ws.js';
// 导入预编译的protobuf定义
import protoBundle from '../crypto/protobuf/bundle.js';
export default class WebSocketClient extends EventEmitter {
constructor(options = {}) {
super();
this.url = options.url || 'ws://localhost:17002';
this.autoReconnect = options.autoReconnect !== false;
this.reconnectInterval = options.reconnectInterval || 5000;
this.maxReconnectAttempts = options.maxReconnectAttempts || 10;
this.subscriptions = new Set();
this.reconnectAttempts = 0;
this.connected = false;
this.ws = null;
this.root = null;
// 初始化protobuf
this.initProtobuf();
}
async initProtobuf() {
try {
// 直接使用导入的JSON定义
this.root = protobuf.Root.fromJSON(protoBundle);
} catch (error) {
this.emit('error', new Error('Failed to initialize protobuf: ' + error.message));
}
}
connect() {
if (this.ws) {
return;
}
try {
this.ws = new WebSocket(this.url);
this.bindEvents();
} catch (error) {
this.emit('error', new Error('Failed to create WebSocket connection: ' + error.message));
}
}
bindEvents() {
if (!this.ws) return;
this.ws.on('open', () => {
this.connected = true;
this.reconnectAttempts = 0;
this.emit('connected');
this.sendHello();
});
this.ws.on('message', (data) => {
try {
this.handleMessage(data);
} catch (error) {
this.emit('error', new Error('Failed to handle message: ' + error.message));
}
});
this.ws.on('close', () => {
this.connected = false;
this.emit('disconnected');
this.handleReconnect();
});
this.ws.on('error', (error) => {
this.emit('error', error);
});
}
handleReconnect() {
if (!this.autoReconnect || this.reconnectAttempts >= this.maxReconnectAttempts) {
return;
}
setTimeout(() => {
this.reconnectAttempts++;
this.connect();
}, this.reconnectInterval);
}
async sendHello() {
const WsMessage = this.root.lookupType('protocol.WsMessage');
const message = WsMessage.create({
type: 1, // CHAIN_HELLO
data: Buffer.from('Hello')
});
this.send(WsMessage.encode(message).finish());
}
async subscribeTx(addresses) {
if (!Array.isArray(addresses) || addresses.length === 0) {
throw new Error('Addresses must be a non-empty array');
}
if (addresses.length > 100) {
throw new Error('Cannot subscribe to more than 100 addresses');
}
const WsMessage = this.root.lookupType('protocol.WsMessage');
const ChainSubscribeTx = this.root.lookupType('protocol.ChainSubscribeTx');
const subscribeMessage = ChainSubscribeTx.create({
address: addresses
});
const message = WsMessage.create({
type: 4, // CHAIN_SUBSCRIBE_TX
data: ChainSubscribeTx.encode(subscribeMessage).finish()
});
addresses.forEach(addr => this.subscriptions.add(addr));
this.send(WsMessage.encode(message).finish());
}
async handleMessage(data) {
const WsMessage = this.root.lookupType('protocol.WsMessage');
const message = WsMessage.decode(new Uint8Array(data));
switch (message.type) {
case 1: // CHAIN_HELLO response
this.emit('hello', message);
break;
case 4: // CHAIN_SUBSCRIBE_TX response
this.emit('subscription', message);
break;
case 5: // CHAIN_TX_ENV_STORE
this.emit('transaction', message);
break;
default:
this.emit('message', message);
}
}
send(data) {
if (!this.connected) {
throw new Error('WebSocket is not connected');
}
try {
this.ws.send(data);
} catch (error) {
this.emit('error', new Error('Failed to send message: ' + error.message));
}
}
close() {
if (this.ws) {
this.autoReconnect = false;
this.ws.close();
this.ws = null;
}
}
}