livechat-widget
Version:
LiveChat Widget for Next.js applications
239 lines (200 loc) • 8.7 kB
text/typescript
import { ChatMessage, WebSocketMessage } from '@/types/chat';
export class WebSocketService {
private socket: WebSocket | null = null;
private messageListeners: ((message: ChatMessage) => void)[] = [];
private connectionListeners: ((connected: boolean) => void)[] = [];
private reconnectInterval: NodeJS.Timeout | null = null;
private reconnectAttempts = 0;
private maxReconnectAttempts = 5;
private reconnectDelay = 3000; // 3 seconds
private receivedMessageIds = new Set<string>();
private roomCode: string | null = null;
private appId: string | null = null;
private userCode: string | null = null;
private deleteMessageListeners: ((messageId: string) => void)[] = [];
constructor(private url: string) {}
connect(roomCode: string, appId: string, userCode: string = 'anonymous'): Promise<boolean> {
if (this.socket) {
this.disconnect();
}
this.receivedMessageIds.clear();
const fullUrl = `${this.url}?room_code=${roomCode}&app_id=${appId}&user_code=${userCode}`;
this.socket = new WebSocket(fullUrl);
this.roomCode = roomCode;
this.appId = appId;
this.userCode = userCode;
return new Promise((resolve) => {
if (this.socket) {
this.socket.onopen = () => {
console.log('WebSocket connection established');
this.reconnectAttempts = 0;
this.notifyConnectionListeners(true);
resolve(true);
};
this.socket.onmessage = (event) => {
try {
const data: WebSocketMessage = JSON.parse(event.data);
// ตรวจสอบประเภทของข้อความ
if (data.type === 'chat' && data.payload) {
// ตรวจสอบว่า payload มีรูปแบบ snake_case
const payload = data.payload as ChatMessage;
// ตรวจสอบว่ามี user_info หรือไม่
if (!payload.user_info) {
payload.user_info = {
username: 'Unknown User',
profile_image: '/image/profile.png'
};
}
// ตรวจสอบว่ามี created_at หรือไม่
if (!payload.created_at) {
payload.created_at = new Date().toISOString();
}
// สร้าง id ถ้าไม่มี
if (!payload.id) {
payload.id = `${payload.user_code}_${new Date(payload.created_at).getTime()}`;
}
// ตรวจสอบว่าข้อความซ้ำกันหรือไม่
if (!this.receivedMessageIds.has(payload.id)) {
this.receivedMessageIds.add(payload.id);
this.notifyMessageListeners(payload);
} else {
console.log('Duplicate message received, ignoring:', payload.id);
}
} else if (data.type === 'message_deleted' && data.payload) {
// รับการแจ้งเตือนการลบข้อความ
const deletePayload = data.payload as {
room_code: string;
app_id: string;
message_id: string;
};
// console.log('Message deleted notification received:', deletePayload);
// แจ้งเตือนผู้ฟังเกี่ยวกับการลบข้อความ
this.notifyDeleteMessageListeners(deletePayload.message_id);
}
} catch (error) {
console.error('Error parsing WebSocket message:', error);
}
};
this.socket.onclose = (event) => {
console.log('WebSocket connection closed:', event.code, event.reason);
this.notifyConnectionListeners(false);
this.socket = null;
// Attempt to reconnect
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectInterval = setTimeout(() => {
this.reconnectAttempts++;
this.connect(this.roomCode!, this.appId!, this.userCode || 'anonymous').then((connected) => {
if (!connected) {
resolve(false);
}
});
}, this.reconnectDelay);
} else {
resolve(false);
}
};
this.socket.onerror = (error) => {
console.error('WebSocket error:', error);
resolve(false);
};
} else {
resolve(false);
}
});
}
disconnect(): void {
if (this.socket) {
this.socket.close();
this.socket = null;
}
// ล้าง receivedMessageIds เมื่อตัดการเชื่อมต่อ
this.receivedMessageIds.clear();
if (this.reconnectInterval) {
clearTimeout(this.reconnectInterval);
this.reconnectInterval = null;
}
}
sendMessage(message: ChatMessage): void {
if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
console.error('WebSocket is not connected');
// ลองเชื่อมต่อใหม่ถ้าไม่มีการเชื่อมต่อ
if (!this.socket) {
console.log('Attempting to reconnect before sending message...');
// ใช้ roomCode และ appId ที่เก็บไว้ในตัวแปรระดับคลาส
const roomCode = this.roomCode;
const appId = this.appId;
const userCode = this.userCode;
if (roomCode && appId && userCode) {
this.connect(roomCode, appId, userCode).then(connected => {
if (connected) {
console.log('Reconnected successfully, sending message...');
this.sendMessage(message);
} else {
console.error('Failed to reconnect, message not sent');
}
});
} else {
console.error('Cannot reconnect: missing roomCode, appId or userCode');
}
}
return;
}
// ตรวจสอบว่า message มี user_info หรือไม่
if (!message.user_info) {
message.user_info = {
username: 'Unknown User',
profile_image: '/image/profile.png'
};
}
const wsMessage: WebSocketMessage = {
type: 'chat',
payload: message,
};
this.socket.send(JSON.stringify(wsMessage));
}
addMessageListener(listener: (message: ChatMessage) => void): void {
this.messageListeners.push(listener);
}
removeMessageListener(listener: (message: ChatMessage) => void): void {
this.messageListeners = this.messageListeners.filter((l) => l !== listener);
}
addConnectionListener(listener: (connected: boolean) => void): void {
this.connectionListeners.push(listener);
}
removeConnectionListener(listener: (connected: boolean) => void): void {
this.connectionListeners = this.connectionListeners.filter((l) => l !== listener);
}
addDeleteMessageListener(listener: (messageId: string) => void): void {
this.deleteMessageListeners.push(listener);
}
removeDeleteMessageListener(listener: (messageId: string) => void): void {
this.deleteMessageListeners = this.deleteMessageListeners.filter(l => l !== listener);
}
private notifyMessageListeners(message: ChatMessage): void {
this.messageListeners.forEach((listener) => listener(message));
}
private notifyConnectionListeners(connected: boolean): void {
this.connectionListeners.forEach((listener) => listener(connected));
}
private notifyDeleteMessageListeners(messageId: string): void {
this.deleteMessageListeners.forEach((listener) => listener(messageId));
}
isConnected(): boolean {
return this.socket !== null && this.socket.readyState === WebSocket.OPEN;
}
}
// สร้าง instance ใหม่ทุกครั้ง
let defaultWsUrl = '';
let wsServiceInstance: WebSocketService | null = null;
// เพิ่มฟังก์ชันสำหรับกำหนด WebSocket URL
export function setWebSocketUrl(url: string): void {
defaultWsUrl = url;
wsServiceInstance = null; // Reset instance when URL changes
}
// ฟังก์ชันสำหรับดึง WebSocketService
export function getWebSocketService(url?: string): WebSocketService {
if (!wsServiceInstance) {
wsServiceInstance = new WebSocketService(url || defaultWsUrl);
}
return wsServiceInstance;
}