/** * THE ZERO-SOCKET ENGINE (FASE 2) * Pengganti murni untuk socket.io-client tanpa dependensi pihak ketiga. * Ini mem-bypass NodeJS WebSockets dan menggunakan Next.js SSE (Server-Sent Events) + Fetch API, * memastikan performa mentah HTTP/2 murni tanpa overhead WebSocket ping/pong. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any type EventCallback = (data: any) => 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: EventCallback) { if (!this.listeners[event]) this.listeners[event] = []; this.listeners[event].push(callback); return this; // for chaining } once(event: string, callback: EventCallback) { const onceWrapper: EventCallback = (data: unknown) => { callback(data); this.off(event, onceWrapper); }; this.on(event, onceWrapper); return this; } off(event: string, callback?: EventCallback) { 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: unknown = {}) { // 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") { const p = payload as { sessionId: string }; this.channel = `qr_session_${p.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") { this.channel = `USER_${payload}`; 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 }) }).catch(e => console.error("[ZeroSocket] Gagal memancarkan sinyal:", e)); 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): ZeroSocket => { const socket = new ZeroSocket(url || ""); socket.connect(); return socket; }; // Export tipe export type Socket = ZeroSocket;