Files
multiverse/jumpa-chat/lib/zero-socket.ts
T

120 lines
3.7 KiB
TypeScript

// [TSM.ID].[11031972] -- All Rights Reserved. Proprietary & Confidential.
export type SocketCallback = (data: unknown) => void;
export class ZeroSocket {
private url: string;
private listeners: Record<string, SocketCallback[]> = {};
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<string, unknown> = {}) {
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<string, unknown>).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, any>).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<string, unknown>).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;