// [TSM.ID].[11031972] -- All Rights Reserved. Proprietary & Confidential. export type SocketCallback = (data: unknown) => void; export class ZeroSocket { private url: string; private listeners: Record = {}; private eventSource: EventSource | null = null; private channel: string = "global_lobby"; public id: string; constructor(url: string) { this.url = url; // Generate random id simulating socket.id this.id = Math.random().toString(36).substring(2, 15); } // Connect is called explicitly or implicitly connect() { if (this.eventSource) return; this.initSSE(this.channel); } private initSSE(channelName: string) { if (this.eventSource) { this.eventSource.close(); } this.eventSource = new EventSource(`/api/omnibrain/sse?channel=${channelName}`); this.eventSource.onmessage = (e) => { try { const data = JSON.parse(e.data); if (data.event && this.listeners[data.event]) { this.listeners[data.event].forEach(fn => fn(data.payload)); } } catch { // Abaikan parse error (mungkin keep-alive) } }; this.eventSource.onerror = () => { console.warn("[ZeroSocket] SSE Koneksi terputus, mencoba memulihkan otomatis..."); }; } on(event: string, callback: SocketCallback) { if (!this.listeners[event]) this.listeners[event] = []; this.listeners[event].push(callback); return this; // for chaining } once(event: string, callback: SocketCallback) { const onceWrapper = (data: unknown) => { callback(data); this.off(event, onceWrapper); }; this.on(event, onceWrapper); return this; } off(event: string, callback?: SocketCallback) { if (!this.listeners[event]) return this; if (callback) { this.listeners[event] = this.listeners[event].filter(cb => cb !== callback); } else { delete this.listeners[event]; } return this; } emit(event: string, payload: string | Record = {}) { const normalizedPayload = typeof payload === 'string' ? { value: payload } : payload; // Simulasi `socket.join(room)` // Di backend socket.io asli, `join` memasukkan client ke room. Di ZeroSocket, kita mengubah parameter channel SSE. if (event === "qr_auth_init") { this.channel = `qr_session_${(payload as Record).sessionId}`; this.initSSE(this.channel); } else if (event === "guest_knock") { // Kita ganti channel menjadi lobby_ROOM agar bisa mendengarkan approval this.channel = `guest_${this.id}`; this.initSSE(this.channel); (payload as Record).guestSocketId = this.id; // Sisipkan ID agar host tahu harus membalas kemana } else if (event === "register_user") { const userId = typeof payload === 'string' ? payload : (payload as Record).userId; this.channel = `USER_${userId}`; this.initSSE(this.channel); } // Fire & Forget HTTP POST ke Redis PubSub OmniBrain fetch('/api/omnibrain/emit', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ channel: this.channel, event, payload: normalizedPayload }) }).catch(() => console.error("[ZeroSocket] Gagal memancarkan sinyal:")); return this; } disconnect() { if (this.eventSource) { this.eventSource.close(); this.eventSource = null; } } } // Named export 'io' untuk meniru "import { io } from 'socket.io-client'" export const io = (url?: string, options?: any) => { const socket = new ZeroSocket(url || ""); socket.connect(); return socket; }; // Export tipe tiruan export type Socket = ZeroSocket;