127 lines
3.8 KiB
TypeScript
127 lines
3.8 KiB
TypeScript
/**
|
|
* 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<string, EventCallback[]> = {};
|
|
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<string, unknown>).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;
|
|
|