Import JC Web IM
This commit is contained in:
@@ -0,0 +1,236 @@
|
||||
import { appConfig } from '@/config';
|
||||
import { HttpImClient } from './httpImClient';
|
||||
|
||||
const HEARTBEAT_INTERVAL_MS = 25000;
|
||||
const RECONNECT_MIN_MS = 1000;
|
||||
const RECONNECT_MAX_MS = 15000;
|
||||
|
||||
function buildWsUrl() {
|
||||
if (appConfig.wsUrl) {
|
||||
return appConfig.wsUrl;
|
||||
}
|
||||
const protocol = appConfig.useWss ? 'wss' : 'ws';
|
||||
const host = appConfig.imServerHost || window.location.hostname;
|
||||
const port = appConfig.imRoutePort ? `:${appConfig.imRoutePort}` : '';
|
||||
const path = appConfig.wsPath?.startsWith('/') ? appConfig.wsPath : `/${appConfig.wsPath || 'ws'}`;
|
||||
return `${protocol}://${host}${port}${path}`;
|
||||
}
|
||||
|
||||
function normalizeConversationId(payload) {
|
||||
const target = payload.target || payload.to || payload.receiver || payload.conversationTarget || payload.from || payload.sender;
|
||||
return payload.conversationId || (target ? `single-${target}` : '');
|
||||
}
|
||||
|
||||
function normalizeIncomingMessage(payload, currentUserId) {
|
||||
const item = payload.message || payload;
|
||||
const from = item.from || item.sender || item.fromUser || '';
|
||||
const target = item.target || item.to || item.receiver || '';
|
||||
const direction = from && String(from) === String(currentUserId) ? 'out' : 'in';
|
||||
const conversationId = item.conversationId || normalizeConversationId({
|
||||
...item,
|
||||
target: direction === 'out' ? target : from
|
||||
});
|
||||
|
||||
if (!conversationId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const status = item.status || item.deliveryStatus || 'sent';
|
||||
|
||||
return {
|
||||
id: item.id || item.messageId || item.messageUid || `${Date.now()}${Math.random().toString(16).slice(2)}`,
|
||||
messageUid: item.messageUid || item.uid || item.messageId || '',
|
||||
conversationId,
|
||||
from,
|
||||
target,
|
||||
direction,
|
||||
type: item.type || 'text',
|
||||
text: item.text || item.content || item.searchableContent || '',
|
||||
status,
|
||||
deliveryStatus: item.deliveryStatus || (direction === 'out' ? status : ''),
|
||||
timestamp: Number(item.timestamp || item.serverTimestamp || Date.now()),
|
||||
displayTimestamp: Number(item.displayTimestamp || item.timestamp || item.serverTimestamp || Date.now()),
|
||||
raw: item
|
||||
};
|
||||
}
|
||||
|
||||
export class LongConnectionImClient extends HttpImClient {
|
||||
constructor() {
|
||||
super();
|
||||
this.socket = null;
|
||||
this.wsUrl = buildWsUrl();
|
||||
this.heartbeatTimer = null;
|
||||
this.reconnectTimer = null;
|
||||
this.reconnectAttempts = 0;
|
||||
this.manualClose = false;
|
||||
}
|
||||
|
||||
getSdkVersion() {
|
||||
return 'JC-LC/0.1';
|
||||
}
|
||||
|
||||
shouldUseMessagePolling() {
|
||||
return !this.connected;
|
||||
}
|
||||
|
||||
async ensureSdk() {
|
||||
if (typeof WebSocket === 'undefined') {
|
||||
throw new Error('当前浏览器不支持 WebSocket 长连接');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
connect(userId, token) {
|
||||
this.currentUser = { userId, token };
|
||||
this.manualClose = false;
|
||||
this.openSocket();
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this.manualClose = true;
|
||||
this.clearReconnectTimer();
|
||||
this.clearHeartbeatTimer();
|
||||
if (this.socket) {
|
||||
this.socket.close();
|
||||
this.socket = null;
|
||||
}
|
||||
super.disconnect();
|
||||
}
|
||||
|
||||
openSocket() {
|
||||
if (this.socket?.readyState === WebSocket.OPEN) {
|
||||
this.sendAuthFrame();
|
||||
return;
|
||||
}
|
||||
if (this.socket?.readyState === WebSocket.CONNECTING) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.emit('connectionStatusChanged', { connected: false, statusText: '长连接连接中' });
|
||||
|
||||
try {
|
||||
this.socket = new WebSocket(this.wsUrl);
|
||||
this.socket.binaryType = 'arraybuffer';
|
||||
} catch (error) {
|
||||
this.emit('connectionStatusChanged', { connected: false, statusText: '长连接创建失败,使用HTTP补偿' });
|
||||
this.scheduleReconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
this.socket.onopen = () => {
|
||||
this.connected = true;
|
||||
this.reconnectAttempts = 0;
|
||||
this.emit('connectionStatusChanged', { connected: true, statusText: '长连接已连接' });
|
||||
this.sendAuthFrame();
|
||||
this.startHeartbeat();
|
||||
this.refreshConversationList();
|
||||
};
|
||||
|
||||
this.socket.onmessage = event => {
|
||||
this.handleFrame(event.data);
|
||||
};
|
||||
|
||||
this.socket.onerror = () => {
|
||||
this.emit('connectionStatusChanged', { connected: false, statusText: '长连接异常,使用HTTP补偿' });
|
||||
};
|
||||
|
||||
this.socket.onclose = () => {
|
||||
this.connected = false;
|
||||
this.clearHeartbeatTimer();
|
||||
this.socket = null;
|
||||
if (this.manualClose) {
|
||||
return;
|
||||
}
|
||||
this.emit('connectionStatusChanged', { connected: false, statusText: '长连接重连中' });
|
||||
this.scheduleReconnect();
|
||||
};
|
||||
}
|
||||
|
||||
sendAuthFrame() {
|
||||
this.sendFrame({
|
||||
type: 'auth',
|
||||
userId: this.currentUser?.userId || '',
|
||||
token: this.currentUser?.token || '',
|
||||
clientId: this.getClientId(),
|
||||
platform: appConfig.platform,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
sendFrame(frame) {
|
||||
if (this.socket?.readyState !== WebSocket.OPEN) {
|
||||
return false;
|
||||
}
|
||||
this.socket.send(JSON.stringify(frame));
|
||||
return true;
|
||||
}
|
||||
|
||||
handleFrame(rawData) {
|
||||
if (rawData instanceof ArrayBuffer) {
|
||||
this.emit('rawFrameReceived', rawData);
|
||||
return;
|
||||
}
|
||||
|
||||
let payload;
|
||||
try {
|
||||
payload = typeof rawData === 'string' ? JSON.parse(rawData) : rawData;
|
||||
} catch (error) {
|
||||
return;
|
||||
}
|
||||
|
||||
const type = payload.type || payload.event || payload.cmd || '';
|
||||
if (type === 'pong' || type === 'auth_ok' || type === 'connected') {
|
||||
return;
|
||||
}
|
||||
if (type === 'contactsChanged') {
|
||||
this.emit('contactsChanged', payload.contacts || []);
|
||||
return;
|
||||
}
|
||||
if (type === 'conversationsChanged') {
|
||||
this.emit('conversationsChanged', payload.conversations || []);
|
||||
return;
|
||||
}
|
||||
if (type === 'messages' && Array.isArray(payload.messages)) {
|
||||
payload.messages
|
||||
.map(message => normalizeIncomingMessage(message, this.currentUser?.userId))
|
||||
.filter(Boolean)
|
||||
.forEach(message => this.emit('messageReceived', message));
|
||||
return;
|
||||
}
|
||||
|
||||
const message = normalizeIncomingMessage(payload, this.currentUser?.userId);
|
||||
if (message) {
|
||||
this.emit(message.direction === 'out' ? 'messageStatusChanged' : 'messageReceived', message);
|
||||
}
|
||||
}
|
||||
|
||||
startHeartbeat() {
|
||||
this.clearHeartbeatTimer();
|
||||
this.heartbeatTimer = window.setInterval(() => {
|
||||
this.sendFrame({ type: 'ping', timestamp: Date.now() });
|
||||
}, HEARTBEAT_INTERVAL_MS);
|
||||
}
|
||||
|
||||
clearHeartbeatTimer() {
|
||||
if (this.heartbeatTimer) {
|
||||
window.clearInterval(this.heartbeatTimer);
|
||||
this.heartbeatTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
scheduleReconnect() {
|
||||
this.clearReconnectTimer();
|
||||
const delay = Math.min(RECONNECT_MAX_MS, RECONNECT_MIN_MS * 2 ** this.reconnectAttempts);
|
||||
this.reconnectAttempts += 1;
|
||||
this.reconnectTimer = window.setTimeout(() => {
|
||||
this.openSocket();
|
||||
}, delay);
|
||||
}
|
||||
|
||||
clearReconnectTimer() {
|
||||
if (this.reconnectTimer) {
|
||||
window.clearTimeout(this.reconnectTimer);
|
||||
this.reconnectTimer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user