D
Engineering

Realtime Events

Socket.IO events — rooms per instanceId dan store_id. Client emit join_instance/join_store/leave_instance. Server emit qrcode, device_status, message_upsert, getmessage, sendmessage, message_status_update, updatechatlist, pushnotif, updateTeamchat. Pola _bubble.

Server Socket.IO berjalan pada port yang sama dengan HTTP (PORT, default 5002), CORS terbuka (*). Diinisialisasi di src/app.ts:32 via SocketService.getInstance().init(server).

Singleton — SocketService

src/service/socketService.ts — singleton via getInstance(). io (Socket.IO server) hanya boleh diinisialisasi sekali:

ts
public init(httpServer: HttpServer) {
  this.io = new SocketIOServer(httpServer, {
    cors: {
      origin: "*",
      methods: ["GET", "POST"]
    }
  });

  this.io.on('connection', (socket: Socket) => {
    // handler client events
  });
}

Rooms

RoomAnggotaEvent yang diterima
instanceId (per device)Frontend yang join_instanceqrcode, device_status, message_upsert, getmessage(+_bubble), sendmessage(+_bubble), message_status_update, updatechatlist(+_bubble)
store_id (per toko)Frontend yang join_storepushnotif
global (semua client)Semua yang connectupdateTeamchat

Event dari Client

EventPayloadFungsi
join_instanceinstanceId (string)Masuk room device. Server langsung membalas device_status berisi status saat itu
join_storestoreId (string)Masuk room store (untuk menerima pushnotif)
leave_instanceinstanceId (string)Keluar dari room device

join_instance handler

ts
socket.on('join_instance', (instanceId: string) => {
  const roomId = String(instanceId);
  socket.join(roomId);

  const currentStatus = WhatsAppService.getSessionStatus(roomId);
  if (currentStatus) {
    socket.emit('device_status', currentStatus);   // langsung balas status
  }
});

Client yang join langsung dapat status device terkini — berguna untuk menampilkan indikator connected/disconnected di UI.

Event dari Server

EventRoomKapanIsi
qrcodeinstanceIdQR baru tersedia{ id, qr (base64), message }
device_statusinstanceIdKoneksi berubah / saat join{ id, status: "open"|"close", message, isConnected, isActivated, user }
message_upsertinstanceIdPesan masuk diterima{ data: <payload pesan masuk> }
getmessageinstanceIdSetelah webhook pesan masuk sukses{ id (terenkripsi), type, id_device, instance, text, data }
getmessage_bubbleinstanceIdSama seperti getmessageidem
message_status_updateinstanceIdStatus ACK pesan berubah{ id (terenkripsi), instance, pesan_id, temp_id, number, status_ack }
sendmessageinstanceIdSetelah kirim pesan{ id, type, number, instance, text, status, role, data, timestamp }
sendmessage_bubbleinstanceIdSama seperti sendmessageidem
updatechatlistinstanceIdRelay /api/update-chatlistpayload chatlist
updatechatlist_bubbleinstanceIdidemidem
pushnotifstore_idRelay /api/push-notifpayload notifikasi
updateTeamchatglobalRelay /api/update-teamchatpayload team chat

qrcode

Dipancarkan saat Baileys memberikan QR baru:

ts
// whatsappService.ts:133-137
SocketService.getInstance().emitTo(instanceId, 'qrcode', {
  id: instanceId,
  qr: qrBase64,            // data:image/png;base64,...
  message: 'Silahkan scan QR Code',
});

Frontend render QR dari qr (base64 data URL).

device_status

Dipancarkan saat koneksi berubah (open/close) dan saat client join_instance:

jsonc
// Open
{
  "id": "0c1a968c-...",
  "status": "open",
  "isConnected": true,
  "isActivated": true,
  "message": "Device Terhubung",
  "user": { "name": "Nama Profil WA", "id": "628123456789" }
}

// Close
{
  "id": "0c1a968c-...",
  "status": "close",
  "isConnected": false,
  "isActivated": false,
  "message": "Koneksi Terputus",
  "user": null
}

message_upsert

Dipancarkan di processMessage sebelum webhook (async, tidak menunggu hasil webhook):

ts
// incomingMessageService.ts:139-141
SocketService.getInstance().emitTo(instanceId, 'message_upsert', {
  data: payload   // payload lengkap pesan masuk
});

getmessage / getmessage_bubble

Dipancarkan di sendWebhook setelah webhook sukses:

ts
// utils/helper.ts:71-87
SocketService.getInstance().emitTo(instanceId, 'getmessage', {
  id: numenc,                  // AES-256-CBC(instanceId + userNumber)
  type: 'getmessage',
  id_device: userNumber,
  instance: instanceId,
  text: messageId,
  data: responseData,          // data dari response backend
});

SocketService.getInstance().emitTo(instanceId, 'getmessage_bubble', { ... });

id = encrypt(instanceId + userNumber) — AES-256-CBC. Lihat Security.

sendmessage / sendmessage_bubble

Dipancarkan setelah kirim pesan (outbound) via emitStatusToSocket:

ts
// whatsappService.ts:860-870
const socketPayload = {
  id: number,
  type: 'sendmessage',
  number: 0,
  instance: instanceId,
  text: pesanId || '',
  status: status,            // 'sent' | 'error'
  role: role || 'user',
  data: dataSource,
  timestamp: new Date().toISOString(),
};

message_status_update

Dipancarkan di sendWebhook setelah webhook status ACK sukses:

ts
// utils/helper.ts:56-68
SocketService.getInstance().emitTo(instanceId, 'message_status_update', {
  id: numenc,                // terenkripsi
  instance: instanceId,
  pesan_id: payload.id,
  temp_id: payload.temp_id ?? null,
  number: userNumber,
  status_ack: payload.status,    // sent/delivered/viewed/played
});

pushnotif

Dipancarkan ke room store_id via NotificationService.handlePushNotification:

ts
// notificationService.ts:82
SocketService.getInstance().emitTo(payload.store_id, 'pushnotif', socketPayload);

Payload:

jsonc
{
  "id": "...",
  "store_id": "...",
  "type": "...",
  "roles": ["admin", "cs"],        // di-split dari string comma-separated
  "team_ids": ["1", "2", "3"],     // di-split dari string comma-separated
  "created_at": "...",
  "notification": { ... },         // di-parse dari JSON string jika perlu
  "readers": [],
  "data": { ... }                  // di-parse dari JSON string jika perlu
}

updatechatlist / updatechatlist_bubble

Dipancarkan ke room instance:

ts
// notificationService.ts:112-113
SocketService.getInstance().emitTo(payload.instance, 'updatechatlist', socketPayload);
SocketService.getInstance().emitTo(payload.instance, 'updatechatlist_bubble', socketPayload);

updateTeamchat

Satu-satunya event global (.emit, bukan .emitTo):

ts
// notificationService.ts:138
SocketService.getInstance().emit('updateTeamchat', socketPayload);

Semua client yang connect menerima event ini — tidak perlu join room.

Pola _bubble

Beberapa event dikirim ganda dengan payload identik:

EventPair
getmessagegetmessage_bubble
sendmessagesendmessage_bubble
updatechatlistupdatechatlist_bubble

API helper

emitTo(room, event, data)

ts
public emitTo(instanceId: string, eventName: string, data: any) {
  if (this.io) {
    this.io.to(instanceId).emit(eventName, data);
  }
}

Kirim ke room tertentu. Dipakai mayoritas event.

emit(event, data) — global

ts
public emit(event: string, data: any) {
  if (this.io) {
    this.io.emit(event, data);
  }
}

Kirim ke semua client. Hanya dipakai updateTeamchat.

Contoh client

js
const socket = io("http://localhost:5002");

// Join room device
socket.emit("join_instance", "0c1a968c-7918-4c29-a524-0ad7c445ee7f");

// Listen QR
socket.on("qrcode", ({ qr, message }) => {
  console.log(message);
  renderQR(qr);   // qr = data:image/png;base64,...
});

// Listen status
socket.on("device_status", (s) => {
  console.log(s.status);   // "open" | "close"
});

// Listen pesan masuk
socket.on("message_upsert", (m) => {
  console.log("Pesan masuk:", m.data.text);
});

// Listen pesan masuk (setelah webhook sukses)
socket.on("getmessage", (m) => {
  console.log("Pesan dari:", m.id_device);
});

// Join room store untuk pushnotif
socket.emit("join_store", "store-123");
socket.on("pushnotif", (n) => {
  console.log("Notif:", n.type);
});

Keamanan

Langkah berikutnya