NON-PRODUCTION

Chat WebSocket — events

Realtime JSON trên chat-api. CMS đi qua admin-api-gateway /api/v1/chat/ws; game vẫn nối thẳng chat-api /ws. Ping server là binary 1 byte. Gửi/xóa/react tin vẫn gọi HTTP/RPC — WS chỉ fan-out.

1. Tổng quan

MụcGiá trị
HostGame: chat-api. CMS: admin-api-gateway (cùng public key JWT)
PathGame GET /ws · CMS GET /api/v1/chat/ws
Game?access_token=<game jwt> — kind trống / game / user
CMSwss://<admin-gateway>/api/v1/chat/ws?access_token=<cms jwt> (gateway mặc định kind=cms)
ProtocolJSON text events + binary ping/pong
Read limit4096 bytes / frame text
1. GET /ws?access_token= 2. Fan-out theo actor/member 3. JSON events 4. Binary ping 0x00 → pong 0x01
HTTP/RPC: Game APIs · CMS APIs. WS không thay send/history.

2. Handshake

GET /ws?access_token=<jwt>                                    // game → chat-api
GET /api/v1/chat/ws?access_token=<cms_jwt>                    // CMS → admin-api-gateway
GET /api/v1/chat/ws?kind=cms&access_token=<cms_jwt>           // tương đương
Kết quảHTTPBody
Token invalid / hết hạn200, không upgrade{"code":4}
Upgrade fail200, không upgrade{"code":3}
OK101 Switching Protocols—

Chat-api: kind trống parse game JWT trước, fail thì thử CMS JWT. kind=game|user chỉ game. kind=cms|admin chỉ CMS. Admin gateway luôn gắn kind=cms nếu client không gửi kind / token_kind.

Sau upgrade, connection được đăng ký theo actor. Server resolve member của conversation để fan-out; không có protocol subscribe/unsubscribe theo conversation.

3. Heartbeat

HướngFrameGiá trị
Server → clientbinary1 byte 0x00 mỗi ~15s
Client → serverbinary1 byte 0x01 (pong)
Cả haitext JSON{"type":"ping"} / {"type":"pong"}
Không pong trong ~25s → server đóng. Có thể gửi JSON ping thay binary. ~20 tin text liên tiếp cũng reset deadline ping (backoff).

4. Client packets

Text JSON chỉ dùng cho heartbeat; field không dùng có thể bỏ.

{
  "type": "subscribe",
  "conversation_id": "p2p:social:user_1001_user_1002"
}
typeconversation_idServer làm gì
subscribebất kỳHiện chưa được socket handler xử lý; không thay đổi fan-out
unsubscribebất kỳHiện chưa được socket handler xử lý; không thay đổi fan-out
ping—Trả {"type":"pong"}, reset idle
pong—Reset idle

JSON lỗi parse bị bỏ qua. Disconnect: server unregister connection khỏi actor.

5. Event envelope

Mọi sự kiện (trừ ping/pong) cùng shape:

{
  "type": "message",
  "conversation_id": "support:1001:1757812345678901234",
  "actor_id": 1001,
  "origin": "a1b2c3d4-....",
  "message": { /* Message, optional */ },
  "conversation": { /* Conversation, optional */ }
}
FieldÝ nghĩa
typeTên event — switch UI theo bảng dưới
conversation_idRoom. Dùng upsert inbox / mở thread
actor_idNgười gây ra (sender, admin claim, user join…). 0 với game_room.created
originUUID instance chat-api (de-dupe multi-pod). Client có thể bỏ
messageCó với message, delete, edit, reaction và system event
conversationCó với mọi fan-out hiện tại
message_id, reaction, countCó với message.reaction.updated
user_id, last_read_seqCó với conversation.read.updated
Fan-out theo member của conversation (hoặc recipient riêng cho invite), không filter theo actor. Client tự bỏ event của chính mình nếu RPC send đã vẽ tin local.

6. Message events

typeKhi nàoDùng field
messageSend thành công (mọi category)message đầy đủ, actor_id = sender
message.deletedUnsend tin của mìnhmessage với deleted=true
message.editedSửa tin hoặc hoàn tất bản dịch supportmessage đã cập nhật
message.reaction.createdGắn / gỡ reaction P2P, self hoặc game roommessage đã cập nhật reactions
message.reaction.updatedĐổi tổng reaction group/channelmessage_id, reaction, count
message.typing.createdActor báo đang gõactor_id, conversation_id
{
  "type": "message",
  "conversation_id": "p2p:social:user_1001_user_1002",
  "actor_id": 1001,
  "origin": "...",
  "message": {
    "id": "66e1ab0c2f1a4b0012345678",
    "conversation_id": "p2p:social:user_1001_user_1002",
    "seq": 8,
    "sender_id": 1001,
    "sender_type": "user",
    "type": "text",
    "content": {
      "text": "hello",
      "attachments": [],
      "p2p_reactions": {},
      "reaction_count": {}
    },
    "deleted": false,
    "created_at": "2026-09-14T03:20:00.123Z"
  }
}

Replace bubble theo message.id (deleted/reacted). Inbox preview = content.text hoặc [image]/[audio]/…

7. Conversation events

typeKhi nàoDùng field
conversation.invitedActor được mời vào P2P/groupconversation; chỉ gửi recipient pending
conversation.user.joinedPending member accept/joinmessage system + conversation
conversation.user.leftMember leave hoặc bị removemessage system + conversation
conversation.settings.updatedĐổi settings conversationmessage system + conversation
conversation.read.updatedMember mark readuser_id, last_read_seq, conversation

Ba event member/settings là system message: message.content.text bằng đúng type event; metadata có actor/user liên quan.

8. Support events

typeKhi nàoactor_id
support.startedUser /chat/support/start tạo ticket mớiuser
support.claimedAdmin claimadmin
support.agent.addedThêm agent role supportadmin thao tác
support.closedĐóng ticketadmin đóng

Các event này kèm conversation (claimed_by, status, participants…) và được gửi tới toàn bộ admin đang online. CMS pending queue: support.started → thêm card; support.claimed → gỡ khỏi pending.

Support event tới mọi admin đang online, kể cả admin chưa là participant. Sau khi claim, admin vẫn nên refresh pending/claimed list để đồng bộ queue.

9. Game room events

typeKhi nàoactor_id
game_room.createdNode tạo / recreate room0
game_room.joinedUser vào bànuser vừa join
game_room.leftUser rời bànuser vừa leave

conversation.metadata.game_id / room_id, reference.id = {gameID}/{roomID}. Client vào bàn nhận event theo membership, rồi gọi history RPC.

10. Payloads conversation / message

Cùng schema REST (snake_case). Timestamp ISO-8601. id message là hex ObjectId.

conversation (rút gọn):
{
  "id": "game_room:tienlen:room-9",
  "type": "group",
  "category": "game_room",
  "name": "Bàn 9",
  "owner_id": 1001,
  "participants": [{ "id": 1001, "type": "user", "role": "member" }],
  "settings": { "only_admin_send": false, "expired_at": "2026-09-14T04:00:00Z" },
  "reference": { "type": "game_room", "id": "tienlen/room-9" },
  "metadata": { "game_id": "tienlen", "room_id": "room-9" },
  "last_seq": 3,
  "status": "active",
  "created_at": "2026-09-14T03:30:00Z",
  "updated_at": "2026-09-14T03:30:00Z"
}

11. Flow client

  1. Có JWT → game GET /ws?access_token=; CMS GET /api/v1/chat/ws?access_token= trên admin gateway.
  2. Nếu HTTP 200 + code=4: refresh token, không giả định đã connected.
  3. On open: binary pong 0x01 khi nhận 0x00.
  4. Song song gọi list RPC/REST để fill inbox (WS không dump history).
  5. Mở thread mới / vào bàn: conversation membership quyết định fan-out; không gửi subscribe.
  6. On message: append nếu seq mới; trùng id thì ignore.
  7. On message.deleted / message.edited / message.reaction.created: patch theo id.
  8. On support.closed / room expired (send lỗi): khóa composer.
  9. Reconnect: history before_seq=0 để bù tin miss.
Gửi tin: RPC/REST send trước (hoặc optimistic local), WS message để peer + confirm seq/id server.

12. Lỗi

Hiện tượngNguyên nhân / xử lý
{"code":4} lúc GET /wsJWT sai/hết hạn — login lại
{"code":3}Upgrade fail — retry
Socket đóng im lặngThiếu pong 25s, queue full, hoặc process restart — reconnect
Không nhận tin room mớiChưa là member hoặc event phát trước lúc socket kết nối — refresh history/list
JSON client > 4096 bytesServer cắt connection

WS không trả domain error của send/delete — lỗi nằm ở RPC/REST (game / cms).