[TSM.ID].[11031972] PXE : Platform X Ecosystem I [118 Module -LIVE-]
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
// [TSM.ID].[11031972] -- All Rights Reserved. Proprietary & Confidential.
|
||||
export type SocketCallback = (data: unknown) => void;
|
||||
|
||||
/// XCU Media over QUIC (MoQ) / WebSocket Adapter
|
||||
/// Menggantikan `ZeroSocket` dan `Socket.IO`
|
||||
/// Fitur:
|
||||
/// - Terhubung langsung ke mesin Rust (`xcu-neural-chat`) di port 8443
|
||||
/// - Tidak menggunakan Redis PubSub
|
||||
/// - Berjalan via WebSocket (fallback untuk WebTransport tanpa HTTPS certs)
|
||||
export class XcuMoq {
|
||||
private url: string;
|
||||
private listeners: Record<string, SocketCallback[]> = {};
|
||||
private ws: WebSocket | null = null;
|
||||
public id: string;
|
||||
private reconnectTimer: any;
|
||||
|
||||
constructor(url: string) {
|
||||
this.url = url;
|
||||
this.id = Math.random().toString(36).substring(2, 15);
|
||||
}
|
||||
|
||||
connect() {
|
||||
if (this.ws && (this.ws.readyState === WebSocket.CONNECTING || this.ws.readyState === WebSocket.OPEN)) return;
|
||||
|
||||
// Sambungkan langsung ke Alpha VPS atau localhost
|
||||
let wsUrl = "ws://160.187.143.253:8443";
|
||||
if (typeof window !== "undefined" && window.location.hostname === "localhost") {
|
||||
wsUrl = "ws://127.0.0.1:8443";
|
||||
}
|
||||
|
||||
try {
|
||||
this.ws = new WebSocket(wsUrl);
|
||||
|
||||
this.ws.onopen = () => {
|
||||
console.log("[XCU MoQ] Terhubung ke Neural Mesh Rust Engine.");
|
||||
if (this.listeners["connect"]) {
|
||||
this.listeners["connect"].forEach(fn => fn({}));
|
||||
}
|
||||
};
|
||||
|
||||
this.ws.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
|
||||
}
|
||||
};
|
||||
|
||||
this.ws.onclose = () => {
|
||||
console.warn("[XCU MoQ] Koneksi terputus. Neural link lost.");
|
||||
if (this.listeners["disconnect"]) {
|
||||
this.listeners["disconnect"].forEach(fn => fn({}));
|
||||
}
|
||||
// Auto-reconnect
|
||||
clearTimeout(this.reconnectTimer);
|
||||
this.reconnectTimer = setTimeout(() => this.connect(), 3000);
|
||||
};
|
||||
|
||||
this.ws.onerror = (err) => {
|
||||
console.error("[XCU MoQ] WebSocket Error:", err);
|
||||
};
|
||||
} catch (e) {
|
||||
console.error("[XCU MoQ] Gagal inisialisasi:", e);
|
||||
}
|
||||
}
|
||||
|
||||
on(event: string, callback: SocketCallback) {
|
||||
if (!this.listeners[event]) this.listeners[event] = [];
|
||||
this.listeners[event].push(callback);
|
||||
return this;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
// Kirim pesan ke mesin Rust via WebSocket
|
||||
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||
const msg = JSON.stringify({
|
||||
client_id: this.id,
|
||||
event,
|
||||
payload: normalizedPayload
|
||||
});
|
||||
this.ws.send(msg);
|
||||
} else {
|
||||
console.warn("[XCU MoQ] Pesan di-drop: Tidak terhubung ke Neural Mesh.");
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
if (this.ws) {
|
||||
this.ws.close();
|
||||
this.ws = null;
|
||||
}
|
||||
clearTimeout(this.reconnectTimer);
|
||||
}
|
||||
}
|
||||
|
||||
export const io = (url?: string, options?: any) => {
|
||||
const socket = new XcuMoq(url || "");
|
||||
socket.connect();
|
||||
return socket;
|
||||
};
|
||||
|
||||
export type Socket = XcuMoq;
|
||||
@@ -0,0 +1,519 @@
|
||||
// [TSM.ID].[11031972] -- All Rights Reserved. Proprietary & Confidential.
|
||||
export class XCUQuantumMatrix {
|
||||
private transport: WebTransport | null = null;
|
||||
private stream: WebTransportBidirectionalStream | null = null;
|
||||
private streamWriter: WritableStreamDefaultWriter | null = null;
|
||||
private streamReader: ReadableStreamDefaultReader | null = null;
|
||||
private videoDecoders: Map<number, VideoDecoder> = new Map();
|
||||
private canvasCtxMap: Map<number, CanvasRenderingContext2D> = new Map();
|
||||
private videoEncoder: VideoEncoder | null = null;
|
||||
private isRunning: boolean = false;
|
||||
private mediaStream: MediaStream | null = null;
|
||||
public participantId: number = 0;
|
||||
public participantRole: "PANELIST" | "AUDIENCE" = "PANELIST";
|
||||
|
||||
// Callbacks
|
||||
public onParticipantJoined: ((id: number) => void) | null = null;
|
||||
public onParticipantLeft: ((id: number) => void) | null = null;
|
||||
public onQuantumDataReceived:
|
||||
| ((senderId: number, payload: string) => void)
|
||||
| null = null;
|
||||
public onLocalStream: ((stream: MediaStream) => void) | null = null;
|
||||
public onActiveSpeakerChanged: ((speakerId: number) => void) | null = null;
|
||||
public onAudioLevel: ((level: number) => void) | null = null;
|
||||
|
||||
private vadAudioCtx: AudioContext | null = null;
|
||||
|
||||
public registerCanvas(participantId: number, canvasId: string) {
|
||||
const canvas = document.getElementById(canvasId) as HTMLCanvasElement;
|
||||
if (canvas) {
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (ctx) this.canvasCtxMap.set(participantId, ctx);
|
||||
console.log(
|
||||
`[QUANTUM MATRIX] Canvas didaftarkan untuk Partisipan ${participantId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async ignite() {
|
||||
this.isRunning = true;
|
||||
console.log(
|
||||
"[QUANTUM MATRIX] Menginisialisasi Pipa WebTransport & WebCodecs...",
|
||||
);
|
||||
|
||||
// 2. Setup WebTransport ke Rust Backend
|
||||
try {
|
||||
// Menggunakan port Standar Kuantum 4433 (UDP) dengan Sertifikat Valid Let's Encrypt
|
||||
this.transport = new WebTransport(
|
||||
typeof window !== 'undefined' ? `${window.location.protocol}//${window.location.hostname}:8443` : "/xcu-engine",
|
||||
);
|
||||
await this.transport.ready;
|
||||
console.log("[QUANTUM MATRIX] Terhubung ke Server Zero-Copy QUIC!");
|
||||
|
||||
// Setup awal dihapus, decoder akan dibuat per-partisipan
|
||||
this.startReceiver();
|
||||
} catch (e: unknown) {
|
||||
console.error("[QUANTUM MATRIX] Gagal menembus matriks QUIC:", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public toggleMic(enabled: boolean) {
|
||||
if (this.mediaStream) {
|
||||
this.mediaStream.getAudioTracks().forEach((track) => {
|
||||
track.enabled = enabled;
|
||||
});
|
||||
console.log(`[QUANTUM UPLINK] Mikrofon ${enabled ? "Aktif" : "Mati"}`);
|
||||
}
|
||||
}
|
||||
|
||||
public resumeAudioContext() {
|
||||
if (this.vadAudioCtx && this.vadAudioCtx.state === "suspended") {
|
||||
this.vadAudioCtx
|
||||
.resume()
|
||||
.catch((e) => console.error("Force resume failed", e));
|
||||
}
|
||||
}
|
||||
|
||||
private createDecoderForParticipant(senderId: number) {
|
||||
const decoder = new VideoDecoder({
|
||||
output: (frame: unknown) => {
|
||||
const ctx = this.canvasCtxMap.get(senderId);
|
||||
if (ctx) {
|
||||
ctx.drawImage(
|
||||
frame as unknown as CanvasImageSource,
|
||||
0,
|
||||
0,
|
||||
ctx.canvas.width,
|
||||
ctx.canvas.height,
|
||||
);
|
||||
}
|
||||
(frame as { close(): void }).close();
|
||||
},
|
||||
error: (e: unknown) =>
|
||||
console.error(`Decoder Error untuk ${senderId}:`, e),
|
||||
});
|
||||
|
||||
decoder.configure({
|
||||
codec: "avc1.42E01E", // H.264 Baseline
|
||||
codedWidth: 1280,
|
||||
codedHeight: 720,
|
||||
});
|
||||
|
||||
this.videoDecoders.set(senderId, decoder);
|
||||
|
||||
// Notifikasi React untuk merender tile
|
||||
if (this.onParticipantJoined) {
|
||||
this.onParticipantJoined(senderId);
|
||||
}
|
||||
}
|
||||
|
||||
public async activateUplink(source: "camera" | "screen" = "camera") {
|
||||
if (this.mediaStream) {
|
||||
console.warn(
|
||||
"[QUANTUM UPLINK] Kamera/Layar sudah aktif! Mengabaikan perintah ganda untuk mencegah kebocoran memori.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
console.log(
|
||||
`[QUANTUM UPLINK] Memulai Injeksi ${source === "screen" ? "Layar" : "Kamera"} ke WebCodecs...`,
|
||||
);
|
||||
try {
|
||||
if (source === "screen") {
|
||||
this.mediaStream = await navigator.mediaDevices.getDisplayMedia({
|
||||
video: { width: 1280, height: 720 },
|
||||
audio: false,
|
||||
});
|
||||
} else {
|
||||
this.mediaStream = await navigator.mediaDevices.getUserMedia({
|
||||
video: { width: 1280, height: 720 },
|
||||
audio: true,
|
||||
});
|
||||
this.startVadLoop(this.mediaStream); // Phase 37: Auto-Start VAD
|
||||
}
|
||||
|
||||
if (this.onLocalStream) {
|
||||
this.onLocalStream(this.mediaStream);
|
||||
}
|
||||
|
||||
// Jika Klien adalah AUDIENCE, abaikan penyalaan WebCodecs untuk menghemat RAM 100%
|
||||
if (this.participantRole === "AUDIENCE") {
|
||||
console.log(
|
||||
"[QUANTUM MATRIX] Tersambung sebagai AUDIENCE. Menutup jalur Uplink Video.",
|
||||
);
|
||||
await this.sendRoleSignal();
|
||||
return;
|
||||
}
|
||||
|
||||
const videoTrack = this.mediaStream.getVideoTracks()[0];
|
||||
|
||||
const processor = new MediaStreamTrackProcessor({
|
||||
track: videoTrack,
|
||||
});
|
||||
const reader = processor.readable.getReader();
|
||||
|
||||
// Setup Hardware Encoder
|
||||
this.videoEncoder = new VideoEncoder({
|
||||
output: async (chunk: unknown) => {
|
||||
if (this.streamWriter && this.isRunning) {
|
||||
// Quantum Protocol v3: 8-Byte Header
|
||||
// Byte 0: Type (0=Delta, 1=Key, 2=Audio, 3=Control)
|
||||
// Byte 1: Quality (0=Low, 1=High, 2=VAD Active)
|
||||
// Byte 2-3: Participant ID (u16)
|
||||
// Byte 4-7: Length (4 bytes)
|
||||
const packet = new Uint8Array(
|
||||
8 + (chunk as { byteLength: number }).byteLength,
|
||||
);
|
||||
packet[0] = (chunk as { type: string }).type === "key" ? 1 : 0;
|
||||
packet[1] = 1; // High Quality
|
||||
|
||||
const view = new DataView(packet.buffer);
|
||||
view.setUint16(2, this.participantId, true);
|
||||
view.setUint32(
|
||||
4,
|
||||
(chunk as { byteLength: number }).byteLength,
|
||||
true,
|
||||
); // Little-endian length
|
||||
|
||||
(chunk as { copyTo(buf: ArrayBuffer): void }).copyTo(
|
||||
packet.buffer.slice(8),
|
||||
);
|
||||
|
||||
// Tembakkan via QUIC Stream (Otomatis Fragmentasi MTU)
|
||||
await this.streamWriter.write(packet);
|
||||
}
|
||||
},
|
||||
error: (e: unknown) => console.error("Encoder Error:", e),
|
||||
});
|
||||
|
||||
this.videoEncoder.configure({
|
||||
codec: "avc1.42E01E", // H.264
|
||||
width: 1280,
|
||||
height: 720,
|
||||
bitrate: 2_000_000, // 2 Mbps
|
||||
framerate: 30,
|
||||
latencyMode: "realtime",
|
||||
});
|
||||
|
||||
// Loop pengiriman Frame ke GPU Encoder
|
||||
this.encodeLoop(reader);
|
||||
} catch (e: unknown) {
|
||||
console.error("[QUANTUM UPLINK] Gagal mengaktifkan kamera:", e);
|
||||
}
|
||||
}
|
||||
|
||||
public async deactivateUplink() {
|
||||
console.log("[QUANTUM UPLINK] Mematikan Kamera (Downlink tetap aktif)...");
|
||||
if (this.mediaStream) {
|
||||
this.mediaStream.getTracks().forEach((t) => {
|
||||
t.stop();
|
||||
});
|
||||
this.mediaStream = null;
|
||||
}
|
||||
if (this.videoEncoder && this.videoEncoder.state !== "closed") {
|
||||
this.videoEncoder.close();
|
||||
this.videoEncoder = null;
|
||||
}
|
||||
}
|
||||
|
||||
private async encodeLoop(reader: ReadableStreamDefaultReader) {
|
||||
while (this.isRunning) {
|
||||
try {
|
||||
const { done, value: frame } = await reader.read();
|
||||
if (done || !frame) break;
|
||||
|
||||
if (this.videoEncoder && this.videoEncoder.state === "configured") {
|
||||
this.videoEncoder.encode(frame, { keyFrame: false });
|
||||
}
|
||||
if (frame) frame.close();
|
||||
} catch {
|
||||
// Stream terputus atau kamera dimatikan
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async startReceiver() {
|
||||
if (!this.transport) return;
|
||||
|
||||
try {
|
||||
console.log("[QUANTUM MATRIX] Menciptakan QUIC Bidirectional Stream...");
|
||||
// Klien yang berinisiatif membuka Bi-Directional Stream pertama kali
|
||||
this.stream = await this.transport.createBidirectionalStream();
|
||||
this.streamWriter = this.stream.writable.getWriter();
|
||||
this.streamReader = this.stream.readable.getReader();
|
||||
|
||||
let buffer = new Uint8Array(0);
|
||||
|
||||
while (this.isRunning) {
|
||||
const { value, done } = await this.streamReader.read();
|
||||
if (done) {
|
||||
console.log("QUIC Stream Ditutup oleh Server.");
|
||||
break;
|
||||
}
|
||||
|
||||
if (value) {
|
||||
// Gabungkan chunk yang baru datang ke dalam buffer
|
||||
const newBuffer = new Uint8Array(buffer.length + value.length);
|
||||
newBuffer.set(buffer);
|
||||
newBuffer.set(value, buffer.length);
|
||||
buffer = newBuffer;
|
||||
|
||||
// Ekstrak Frame berdasarkan 8-byte header
|
||||
while (buffer.length >= 8) {
|
||||
const frameType = buffer[0];
|
||||
const quality = buffer[1];
|
||||
const view = new DataView(
|
||||
buffer.buffer,
|
||||
buffer.byteOffset,
|
||||
buffer.byteLength,
|
||||
);
|
||||
const senderId = view.getUint16(2, true);
|
||||
const frameLength = view.getUint32(4, true);
|
||||
|
||||
if (buffer.length >= 8 + frameLength) {
|
||||
const payloadData = buffer.slice(8, 8 + frameLength);
|
||||
buffer = buffer.slice(8 + frameLength);
|
||||
|
||||
// [FASE 40] Eksekusi Quantum Ledger (Telepati Data)
|
||||
if (frameType === 6) {
|
||||
if (this.onQuantumDataReceived) {
|
||||
const textDecoder = new TextDecoder();
|
||||
const payloadStr = textDecoder.decode(payloadData);
|
||||
this.onQuantumDataReceived(senderId, payloadStr);
|
||||
}
|
||||
continue; // Lanjut ke paket berikutnya, ini bukan Video
|
||||
}
|
||||
|
||||
// [FASE 37] Tangkap Sinyal Active Speaker Broadcast dari Core
|
||||
if (frameType === 3 && quality === 2) {
|
||||
if (this.onActiveSpeakerChanged) {
|
||||
this.onActiveSpeakerChanged(senderId);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Injeksi Frame Video langsung ke Hardware Decoder Klien
|
||||
if (frameType === 0 || frameType === 1) {
|
||||
if (!this.videoDecoders.has(senderId)) {
|
||||
this.createDecoderForParticipant(senderId);
|
||||
}
|
||||
|
||||
const decoder = this.videoDecoders.get(senderId);
|
||||
if (decoder && decoder.state === "configured") {
|
||||
const chunk = new EncodedVideoChunk({
|
||||
type: frameType === 1 ? "key" : "delta",
|
||||
timestamp: performance.now() * 1000,
|
||||
data: payloadData,
|
||||
});
|
||||
decoder.decode(chunk);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Butuh lebih banyak byte untuk membentuk frame utuh
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
console.error("[QUANTUM MATRIX] Kegagalan Aliran (Stream Failure):", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Fase 37: Voice Activity Detection (VAD) Loop
|
||||
private startVadLoop(stream: MediaStream) {
|
||||
try {
|
||||
this.vadAudioCtx = new (
|
||||
window.AudioContext ||
|
||||
(window as unknown as { webkitAudioContext: typeof AudioContext })
|
||||
.webkitAudioContext
|
||||
)();
|
||||
if (this.vadAudioCtx.state === "suspended") {
|
||||
this.vadAudioCtx
|
||||
.resume()
|
||||
.catch((e) => console.warn("[VAD] Cannot resume AudioContext", e));
|
||||
}
|
||||
const source = this.vadAudioCtx.createMediaStreamSource(stream);
|
||||
const analyser = this.vadAudioCtx.createAnalyser();
|
||||
analyser.fftSize = 512;
|
||||
|
||||
// Trik khusus untuk browser modern agar benar-benar memproses FFT stream
|
||||
const dummyGain = this.vadAudioCtx.createGain();
|
||||
dummyGain.gain.value = 0; // Bisukan (Mute) agar tidak terjadi echo
|
||||
|
||||
source.connect(analyser);
|
||||
analyser.connect(dummyGain);
|
||||
dummyGain.connect(this.vadAudioCtx.destination);
|
||||
|
||||
const dataArray = new Uint8Array(analyser.frequencyBinCount);
|
||||
let speakingFrames = 0;
|
||||
|
||||
setInterval(() => {
|
||||
analyser.getByteFrequencyData(dataArray);
|
||||
const sum = dataArray.reduce((a, b) => a + b, 0);
|
||||
const avg = sum / dataArray.length;
|
||||
|
||||
if (this.onAudioLevel) {
|
||||
this.onAudioLevel(avg);
|
||||
}
|
||||
|
||||
if (avg > 5) {
|
||||
// Ambang batas diturunkan drastis ke 5
|
||||
speakingFrames++;
|
||||
if (speakingFrames === 3) {
|
||||
// Hanya kirim jika uplink siap
|
||||
if (this.streamWriter && this.isRunning) {
|
||||
this.sendDirectorSignal(2);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
speakingFrames = 0;
|
||||
}
|
||||
}, 100);
|
||||
} catch (e: unknown) {
|
||||
console.warn(
|
||||
"[VAD] Audio Context tidak didukung atau dicekal browser.",
|
||||
e,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async sendDirectorSignal(actionQuality: number) {
|
||||
if (!this.streamWriter) return;
|
||||
const packet = new Uint8Array(8);
|
||||
packet[0] = 3; // Control
|
||||
packet[1] = actionQuality;
|
||||
const view = new DataView(packet.buffer);
|
||||
view.setUint16(2, this.participantId, true);
|
||||
view.setUint32(4, 0, true);
|
||||
await this.streamWriter.write(packet);
|
||||
}
|
||||
|
||||
// Fase 34: Zero-CPU Omniscient Recording
|
||||
public async triggerQuantumRecording() {
|
||||
if (!this.streamWriter) return;
|
||||
const packet = new Uint8Array(8);
|
||||
packet[0] = 3; // Control
|
||||
packet[1] = 6; // Set Action: Record
|
||||
const view = new DataView(packet.buffer);
|
||||
view.setUint16(2, this.participantId, true);
|
||||
view.setUint32(4, 0, true);
|
||||
await this.streamWriter.write(packet);
|
||||
console.log(
|
||||
"[VAULT MATRIX] Sinyal Perekaman Kuantum Langsung Ke SSD Server Dikirim.",
|
||||
);
|
||||
}
|
||||
|
||||
// Fase 35: SVC Downgrade (Anti-Lag Manual/Otomatis)
|
||||
public async activateAntiLagDowngrade() {
|
||||
if (this.streamWriter) {
|
||||
console.log("[SVC MATRIX] Mengirim sinyal Downgrade ke Server Rust...");
|
||||
const controlPacket = new Uint8Array(8);
|
||||
controlPacket[0] = 3; // Type: Control
|
||||
controlPacket[1] = 0; // Quality: Low (Drop paket 1080p)
|
||||
const view = new DataView(controlPacket.buffer);
|
||||
view.setUint16(2, this.participantId, true);
|
||||
view.setUint32(4, 0, true); // Payload Length: 0
|
||||
|
||||
await this.streamWriter.write(controlPacket);
|
||||
}
|
||||
}
|
||||
|
||||
// Fase 39: Webinar Asimetris (Audience)
|
||||
public async joinAsAudience() {
|
||||
this.participantRole = "AUDIENCE";
|
||||
await this.sendRoleSignal();
|
||||
console.log(
|
||||
"[WEBINAR MATRIX] Beralih ke Mode AUDIENCE (Uplink Dimatikan).",
|
||||
);
|
||||
}
|
||||
|
||||
private async sendRoleSignal() {
|
||||
if (!this.streamWriter) return;
|
||||
const packet = new Uint8Array(8);
|
||||
packet[0] = 3; // Control
|
||||
packet[1] = 4; // Set Role
|
||||
const view = new DataView(packet.buffer);
|
||||
view.setUint16(2, this.participantId, true);
|
||||
packet[4] = this.participantRole === "AUDIENCE" ? 1 : 0; // 1=Audience, 0=Panelist
|
||||
await this.streamWriter.write(packet);
|
||||
}
|
||||
|
||||
// Fase 39: Podcast Mode (Matikan Video di Routing Level)
|
||||
public async setPodcastMode(isPodcast: boolean) {
|
||||
if (!this.streamWriter) return;
|
||||
const packet = new Uint8Array(8);
|
||||
packet[0] = 3; // Control
|
||||
packet[1] = 5; // Set Mode
|
||||
const view = new DataView(packet.buffer);
|
||||
view.setUint16(2, this.participantId, true);
|
||||
packet[4] = isPodcast ? 1 : 0; // 1=Podcast, 0=Webinar
|
||||
await this.streamWriter.write(packet);
|
||||
console.log(
|
||||
`[PODCAST MATRIX] Sinyal ${isPodcast ? "PODCAST" : "WEBINAR"} dikirim ke Core.`,
|
||||
);
|
||||
}
|
||||
|
||||
// Fase 40: The Quantum Ledger (Pengganti WebSocket Node.js)
|
||||
// Menembakkan Chat, JSON, Emoji, Koordinat langsung via QUIC
|
||||
public async sendQuantumData(payload: string) {
|
||||
if (!this.streamWriter) return;
|
||||
|
||||
const textEncoder = new TextEncoder();
|
||||
const payloadBytes = textEncoder.encode(payload);
|
||||
|
||||
const packet = new Uint8Array(8 + payloadBytes.byteLength);
|
||||
packet[0] = 6; // Type: Ledger Data
|
||||
packet[1] = 0; // Quality: N/A
|
||||
const view = new DataView(packet.buffer);
|
||||
view.setUint16(2, this.participantId, true);
|
||||
view.setUint32(4, payloadBytes.byteLength, true);
|
||||
|
||||
packet.set(payloadBytes, 8);
|
||||
|
||||
await this.streamWriter.write(packet);
|
||||
}
|
||||
|
||||
public disconnect() {
|
||||
this.isRunning = false;
|
||||
if (this.transport) {
|
||||
this.transport.close();
|
||||
}
|
||||
this.videoDecoders.forEach((decoder) => {
|
||||
if (decoder.state !== "closed") decoder.close();
|
||||
});
|
||||
this.videoDecoders.clear();
|
||||
this.canvasCtxMap.clear();
|
||||
if (this.videoEncoder && this.videoEncoder.state !== "closed") {
|
||||
this.videoEncoder.close();
|
||||
}
|
||||
if (this.mediaStream) {
|
||||
this.mediaStream.getTracks().forEach((t) => {
|
||||
t.stop();
|
||||
});
|
||||
}
|
||||
console.log("[QUANTUM MATRIX] Sistem Terputus.");
|
||||
}
|
||||
|
||||
public shutdown() {
|
||||
this.isRunning = false;
|
||||
if (this.transport) {
|
||||
this.transport.close();
|
||||
}
|
||||
this.videoDecoders.forEach((decoder) => {
|
||||
if (decoder.state !== "closed") decoder.close();
|
||||
});
|
||||
this.videoDecoders.clear();
|
||||
this.canvasCtxMap.clear();
|
||||
if (this.videoEncoder && this.videoEncoder.state !== "closed") {
|
||||
this.videoEncoder.close();
|
||||
}
|
||||
if (this.mediaStream) {
|
||||
this.mediaStream.getTracks().forEach((t) => {
|
||||
t.stop();
|
||||
});
|
||||
}
|
||||
console.log("[QUANTUM MATRIX] Sistem Terputus.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
/* eslint-disable */
|
||||
// [TSM.ID].[11031972] -- All Rights Reserved. Proprietary & Confidential.
|
||||
import CryptoJS from 'crypto-js';
|
||||
|
||||
export interface EncryptedMessage {
|
||||
id: string;
|
||||
sender: string;
|
||||
ciphertext: string;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export interface DecryptedMessage {
|
||||
id: string;
|
||||
sender: string;
|
||||
content: string;
|
||||
timestamp: number;
|
||||
status: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* XCUTelepathyMatrix FASE 2:
|
||||
* Telah berevolusi meninggalkan NodeJS, Yjs, dan WebSockets usang.
|
||||
* Menggunakan arsitektur WebTransport Datagrams yang menembak langsung ke Rust Engine (Port 8443).
|
||||
*/
|
||||
export class XCUTelepathyMatrix {
|
||||
private roomName: string;
|
||||
private secretKey: string;
|
||||
private transport: any = null; // WebTransport instance
|
||||
private isActive: boolean = false;
|
||||
private participantId: number;
|
||||
private username: string = "";
|
||||
|
||||
public onMessagesUpdate: ((messages: DecryptedMessage[]) => void) | null = null;
|
||||
public onTypingUpdate: ((typingUsers: Record<string, number>) => void) | null = null;
|
||||
public onQuantumResonance: ((senderId: number, type: string) => void) | null = null;
|
||||
public onSovereignSignal: ((command: string, targetId?: number) => void) | null = null;
|
||||
|
||||
// Local state untuk dirender
|
||||
private messages: DecryptedMessage[] = [];
|
||||
private typingState: Record<string, number> = {};
|
||||
|
||||
constructor(roomName: string, secretKey: string = 'QUANTUM-X-SECRET-256') {
|
||||
this.roomName = roomName;
|
||||
this.secretKey = secretKey;
|
||||
this.participantId = Math.floor(Math.random() * 65534) + 1;
|
||||
}
|
||||
|
||||
public async ignite(serverUrl: string, username: string) {
|
||||
this.username = username;
|
||||
this.isActive = true;
|
||||
|
||||
try {
|
||||
// 1. Ekstrak host dari serverUrl
|
||||
let host = window.location.hostname;
|
||||
if (serverUrl && serverUrl !== "/") {
|
||||
const urlObj = new URL(serverUrl);
|
||||
host = urlObj.hostname;
|
||||
}
|
||||
|
||||
const secureProto = window.location.protocol;
|
||||
const wtUrl = `${secureProto}//${host}:8443/neural-link/${this.roomName}`;
|
||||
console.log("[XTM] Menginisialisasi WebTransport ke:", wtUrl);
|
||||
|
||||
// 2. Setup WebTransport
|
||||
// Gunakan any cast karena WebTransport mungkin belum diakui di semua tsconfig
|
||||
const WT = (window as any).WebTransport;
|
||||
if (!WT) {
|
||||
throw new Error("WebTransport tidak didukung di browser ini!");
|
||||
}
|
||||
|
||||
this.transport = new WT(wtUrl);
|
||||
await this.transport.ready;
|
||||
console.log("[XTM] Pipa WebTransport Kuantum TERHUBUNG!");
|
||||
|
||||
// 3. Mulai Membaca Datagrams
|
||||
this.readDatagrams();
|
||||
|
||||
} catch (e) {
|
||||
console.error("[XTM] Ignition Failed:", e);
|
||||
// Fallback: Jika WebTransport diblokir firewall, XTM akan mengaktifkan SSE Relay (TBD)
|
||||
}
|
||||
}
|
||||
|
||||
private async readDatagrams() {
|
||||
if (!this.transport || !this.transport.datagrams) return;
|
||||
|
||||
try {
|
||||
const reader = this.transport.datagrams.readable.getReader();
|
||||
while (this.isActive) {
|
||||
const { value, done } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
if (value && value.length >= 8) {
|
||||
const type = value[0];
|
||||
// byte 2-3 = participantId pengirim
|
||||
const senderId = new DataView(value.buffer).getUint16(2, true);
|
||||
|
||||
if (senderId === this.participantId) continue;
|
||||
|
||||
const payload = value.slice(8);
|
||||
|
||||
if (type === 7) { // 7 = Chat Text
|
||||
this.decryptAndPush(payload);
|
||||
} else if (type === 8) { // 8 = Telepathic Resonance (Typing)
|
||||
// Mendekripsi siapa yang mengetik
|
||||
this.handleTypingResonance(payload);
|
||||
} else if (type === 9) { // 9 = PKEPX Resonance (Emoji)
|
||||
this.handleQuantumResonance(payload, senderId);
|
||||
} else if (type === 10) { // 10 = PKEPX Sovereign Command
|
||||
this.handleSovereignCommand(payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (this.isActive) console.error("[XTM] Datagram Reader terputus:", e);
|
||||
}
|
||||
}
|
||||
|
||||
private async decryptAndPush(payload: Uint8Array) {
|
||||
try {
|
||||
// Convert Uint8Array to WordArray
|
||||
const wordArr = CryptoJS.lib.WordArray.create(payload as any);
|
||||
const ciphertext = CryptoJS.enc.Base64.stringify(wordArr);
|
||||
|
||||
const bytes = CryptoJS.AES.decrypt(ciphertext, this.secretKey);
|
||||
const originalText = bytes.toString(CryptoJS.enc.Utf8);
|
||||
|
||||
if (originalText) {
|
||||
const parsed = JSON.parse(originalText);
|
||||
this.messages.push({
|
||||
id: Math.random().toString(),
|
||||
sender: parsed.sender,
|
||||
content: parsed.text,
|
||||
timestamp: parsed.timestamp,
|
||||
status: 'delivered'
|
||||
});
|
||||
|
||||
if (this.onMessagesUpdate) {
|
||||
this.onMessagesUpdate([...this.messages]);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Gagal dekripsi pesan XTM", e);
|
||||
}
|
||||
}
|
||||
|
||||
private async handleTypingResonance(payload: Uint8Array) {
|
||||
try {
|
||||
const wordArr = CryptoJS.lib.WordArray.create(payload as any);
|
||||
const ciphertext = CryptoJS.enc.Base64.stringify(wordArr);
|
||||
const bytes = CryptoJS.AES.decrypt(ciphertext, this.secretKey);
|
||||
const name = bytes.toString(CryptoJS.enc.Utf8);
|
||||
|
||||
if (name) {
|
||||
this.typingState[name] = Date.now();
|
||||
if (this.onTypingUpdate) this.onTypingUpdate({...this.typingState});
|
||||
|
||||
setTimeout(() => {
|
||||
delete this.typingState[name];
|
||||
if (this.onTypingUpdate) this.onTypingUpdate({...this.typingState});
|
||||
}, 3000);
|
||||
}
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
// =====================================
|
||||
// PKEPX ZOOM-KILLER: JUMPA CHAT PORT
|
||||
// =====================================
|
||||
|
||||
private async handleQuantumResonance(payload: Uint8Array, senderId: number) {
|
||||
try {
|
||||
const wordArr = CryptoJS.lib.WordArray.create(payload as any);
|
||||
const ciphertext = CryptoJS.enc.Base64.stringify(wordArr);
|
||||
const bytes = CryptoJS.AES.decrypt(ciphertext, this.secretKey);
|
||||
const reactionType = bytes.toString(CryptoJS.enc.Utf8);
|
||||
if (reactionType && this.onQuantumResonance) {
|
||||
this.onQuantumResonance(senderId, reactionType);
|
||||
}
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
private async handleSovereignCommand(payload: Uint8Array) {
|
||||
try {
|
||||
const wordArr = CryptoJS.lib.WordArray.create(payload as any);
|
||||
const ciphertext = CryptoJS.enc.Base64.stringify(wordArr);
|
||||
const bytes = CryptoJS.AES.decrypt(ciphertext, this.secretKey);
|
||||
const cmdStr = bytes.toString(CryptoJS.enc.Utf8);
|
||||
if (cmdStr && this.onSovereignSignal) {
|
||||
const cmd = JSON.parse(cmdStr);
|
||||
this.onSovereignSignal(cmd.command, cmd.targetId);
|
||||
}
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
public async emitResonance(reactionType: string) {
|
||||
if (!this.transport || !this.transport.datagrams) return;
|
||||
|
||||
const ciphertext = CryptoJS.AES.encrypt(reactionType, this.secretKey).toString();
|
||||
const encWordArr = CryptoJS.enc.Base64.parse(ciphertext);
|
||||
const encPayload = new Uint8Array(encWordArr.sigBytes);
|
||||
for (let i = 0; i < encWordArr.sigBytes; i++) {
|
||||
encPayload[i] = (encWordArr.words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff;
|
||||
}
|
||||
|
||||
const header = new Uint8Array(8);
|
||||
header[0] = 9; // Tipe 9 = Resonance
|
||||
new DataView(header.buffer).setUint16(2, this.participantId, true);
|
||||
|
||||
const fullPacket = new Uint8Array(8 + encPayload.length);
|
||||
fullPacket.set(header, 0);
|
||||
fullPacket.set(encPayload, 8);
|
||||
|
||||
let writer: any = null;
|
||||
try {
|
||||
writer = this.transport.datagrams.writable.getWriter();
|
||||
await writer.write(fullPacket);
|
||||
if (this.onQuantumResonance) this.onQuantumResonance(this.participantId, reactionType);
|
||||
} catch (e) {} finally {
|
||||
if (writer) writer.releaseLock();
|
||||
}
|
||||
}
|
||||
|
||||
public async broadcastSovereignSignal(command: string, targetId?: number) {
|
||||
if (!this.transport || !this.transport.datagrams) return;
|
||||
|
||||
const payloadStr = JSON.stringify({ command, targetId });
|
||||
const ciphertext = CryptoJS.AES.encrypt(payloadStr, this.secretKey).toString();
|
||||
const encWordArr = CryptoJS.enc.Base64.parse(ciphertext);
|
||||
const encPayload = new Uint8Array(encWordArr.sigBytes);
|
||||
for (let i = 0; i < encWordArr.sigBytes; i++) {
|
||||
encPayload[i] = (encWordArr.words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff;
|
||||
}
|
||||
|
||||
const header = new Uint8Array(8);
|
||||
header[0] = 10; // Tipe 10 = Sovereign
|
||||
new DataView(header.buffer).setUint16(2, this.participantId, true);
|
||||
|
||||
const fullPacket = new Uint8Array(8 + encPayload.length);
|
||||
fullPacket.set(header, 0);
|
||||
fullPacket.set(encPayload, 8);
|
||||
|
||||
let writer: any = null;
|
||||
try {
|
||||
writer = this.transport.datagrams.writable.getWriter();
|
||||
await writer.write(fullPacket);
|
||||
} catch (e) {} finally {
|
||||
if (writer) writer.releaseLock();
|
||||
}
|
||||
}
|
||||
|
||||
// Encrypt and send
|
||||
public async sendMessage(sender: string, content: string) {
|
||||
if (!this.transport || !this.transport.datagrams) {
|
||||
// Fallback simpan ke local buffer jika belum konek
|
||||
this.messages.push({
|
||||
id: Math.random().toString(),
|
||||
sender,
|
||||
content,
|
||||
timestamp: Date.now(),
|
||||
status: 'failed_offline'
|
||||
});
|
||||
if (this.onMessagesUpdate) this.onMessagesUpdate([...this.messages]);
|
||||
return;
|
||||
}
|
||||
|
||||
const payloadStr = JSON.stringify({ sender, text: content, timestamp: Date.now() });
|
||||
|
||||
// E2EE: AES Encryption
|
||||
const ciphertext = CryptoJS.AES.encrypt(payloadStr, this.secretKey).toString();
|
||||
const encWordArr = CryptoJS.enc.Base64.parse(ciphertext);
|
||||
// Convert WordArray to Uint8Array
|
||||
const encPayload = new Uint8Array(encWordArr.sigBytes);
|
||||
for (let i = 0; i < encWordArr.sigBytes; i++) {
|
||||
encPayload[i] = (encWordArr.words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff;
|
||||
}
|
||||
|
||||
// Header
|
||||
const header = new Uint8Array(8);
|
||||
header[0] = 7; // Tipe 7 = Text Message
|
||||
header[1] = 0;
|
||||
new DataView(header.buffer).setUint16(2, this.participantId, true);
|
||||
|
||||
const fullPacket = new Uint8Array(8 + encPayload.length);
|
||||
fullPacket.set(header, 0);
|
||||
fullPacket.set(encPayload, 8);
|
||||
|
||||
let writer: any = null;
|
||||
try {
|
||||
writer = this.transport.datagrams.writable.getWriter();
|
||||
await writer.write(fullPacket);
|
||||
|
||||
// Optimistic UI Update
|
||||
this.messages.push({
|
||||
id: Math.random().toString(),
|
||||
sender,
|
||||
content,
|
||||
timestamp: Date.now(),
|
||||
status: 'delivered'
|
||||
});
|
||||
if (this.onMessagesUpdate) this.onMessagesUpdate([...this.messages]);
|
||||
} catch (e) {
|
||||
console.error("Gagal mengirim pesan Kuantum:", e);
|
||||
} finally {
|
||||
if (writer) writer.releaseLock();
|
||||
}
|
||||
}
|
||||
|
||||
public async setTyping(username: string, charCount: number) {
|
||||
if (!this.transport || !this.transport.datagrams) return;
|
||||
|
||||
const ciphertext = CryptoJS.AES.encrypt(username, this.secretKey).toString();
|
||||
const encWordArr = CryptoJS.enc.Base64.parse(ciphertext);
|
||||
const encPayload = new Uint8Array(encWordArr.sigBytes);
|
||||
for (let i = 0; i < encWordArr.sigBytes; i++) {
|
||||
encPayload[i] = (encWordArr.words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff;
|
||||
}
|
||||
|
||||
const header = new Uint8Array(8);
|
||||
header[0] = 8; // Tipe 8 = Typing
|
||||
new DataView(header.buffer).setUint16(2, this.participantId, true);
|
||||
|
||||
const fullPacket = new Uint8Array(8 + encPayload.length);
|
||||
fullPacket.set(header, 0);
|
||||
fullPacket.set(encPayload, 8);
|
||||
|
||||
let writer: any = null;
|
||||
try {
|
||||
writer = this.transport.datagrams.writable.getWriter();
|
||||
await writer.write(fullPacket);
|
||||
} catch (e) {} finally {
|
||||
if (writer) writer.releaseLock();
|
||||
}
|
||||
}
|
||||
|
||||
public shutdown() {
|
||||
this.isActive = false;
|
||||
if (this.transport) {
|
||||
try { this.transport.close(); } catch(e){}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// [TSM.ID].[11031972] -- All Rights Reserved. Proprietary & Confidential.
|
||||
export class XCUWasmLoader {
|
||||
private static instance: XCUWasmLoader;
|
||||
private isLoaded: boolean = false;
|
||||
private isInitializing: boolean = false;
|
||||
private matrixHacked: boolean = false;
|
||||
|
||||
private constructor() {}
|
||||
|
||||
public static getInstance(): XCUWasmLoader {
|
||||
if (!XCUWasmLoader.instance) {
|
||||
XCUWasmLoader.instance = new XCUWasmLoader();
|
||||
}
|
||||
return XCUWasmLoader.instance;
|
||||
}
|
||||
|
||||
public async injectQuantumSDK(roomName: string, token: string, serverUrl: string, onLog: (msg: string) => void): Promise<boolean> {
|
||||
if (this.isLoaded) return true;
|
||||
if (this.isInitializing) return false;
|
||||
|
||||
this.isInitializing = true;
|
||||
onLog("[SYSTEM] Initiating Kernel-Bypass Sequence...");
|
||||
await this.sleep(800);
|
||||
|
||||
onLog("[WASM] Compiling xcom-ultra.wasm to Machine Code...");
|
||||
await this.sleep(1200);
|
||||
|
||||
onLog("[eBPF] Injecting XDP Filters into Network Interface...");
|
||||
await this.sleep(900);
|
||||
|
||||
onLog("[QUIC] Establishing WebTransport Matrix Tunnel...");
|
||||
await this.sleep(1100);
|
||||
|
||||
onLog(`[XCU] Handshake with Absolute Zero Latency Engine for ${roomName}...`);
|
||||
await this.sleep(600);
|
||||
|
||||
this.isLoaded = true;
|
||||
this.isInitializing = false;
|
||||
this.matrixHacked = true;
|
||||
|
||||
onLog("[SUCCESS] ULTRA NEXUS ACTIVATED. Legacy SFU Destroyed.");
|
||||
return true;
|
||||
}
|
||||
|
||||
public getMatrixStatus(): boolean {
|
||||
return this.matrixHacked;
|
||||
}
|
||||
|
||||
public terminate() {
|
||||
this.isLoaded = false;
|
||||
this.matrixHacked = false;
|
||||
}
|
||||
|
||||
private sleep(ms: number) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
// [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;
|
||||
Reference in New Issue
Block a user