
# **WECHESS**

**FRONTEND GUIDE FOR AI CODING AGENTS - PART 8 - Gameplay Service Realtime Hubs**

This document is a part of a REST API guide for the wechess project.
It is designed for AI agents that will generate frontend code to consume the project's backend.

This document provides the **Realtime Hub** integration guide for the **Gameplay** service.
Realtime Hubs use **Socket.IO** for bidirectional communication between clients and the server, enabling features like chat rooms, live collaboration, game lobbies, and dashboards.

## Connection Setup

### Service URLs

The Socket.IO server runs on the same host as the REST API for the **gameplay** service:

* **Preview:** `https://wechess.prw.mindbricks.com/gameplay-api`
* **Staging:** `https://wechess-stage.mindbricks.co/gameplay-api`
* **Production:** `https://wechess.mindbricks.co/gameplay-api`

### Authentication

Every Socket.IO connection **must** include the user's access token and the correct `path` for the reverse proxy:

```js
import { io } from "socket.io-client";

const socket = io("{baseUrl}/hub/{hubName}", {
  path: "/gameplay-api/socket.io/",  // HTTP transport path (reverse proxy)
  auth: { token: accessToken },           // Bearer token from login
  transports: ["websocket", "polling"],    // prefer websocket
});
```

Where `{baseUrl}` is the service base URL (e.g., `https://wechess.mindbricks.co/gameplay-api`).

**How Socket.IO works behind a reverse proxy:**
- The URL passed to `io()` is `{baseUrl}/hub/{hubName}` where `{baseUrl}` already includes the service prefix (e.g., `/gameplay-api`). Socket.IO extracts the full path after the host as the **namespace** (`/gameplay-api/hub/{hubName}`). Namespaces are logical channels negotiated inside the Socket.IO protocol — the reverse proxy does not affect them.
- The `path` option (`/gameplay-api/socket.io/`) is the **HTTP endpoint** the browser sends for the Socket.IO handshake, polling, and WebSocket upgrade. The reverse proxy routes this to the correct service and strips the prefix, so the server internally matches on the default `/socket.io/` path.

The server validates the token and resolves the user session before allowing any interaction. If the token is invalid or missing, the connection is rejected with an `"Authentication required"` or `"Invalid token"` error.


### Connection Lifecycle

```
connect  →  authenticate  →  join rooms  →  send/receive  →  leave rooms  →  disconnect
```

Listen for connection events:

```js
socket.on("connect", () => {
  console.log("Connected to hub", socket.id);
});

socket.on("connect_error", (err) => {
  console.error("Connection failed:", err.message);
  // Handle re-auth if token expired
});

socket.on("disconnect", (reason) => {
  console.log("Disconnected:", reason);
});
```

## Protocol Reference

All hub events use the `hub:` prefix. Below is the complete protocol that applies to every hub in this service.

### Room Management

| Event | Direction | Payload | Description |
|-------|-----------|---------|-------------|
| `hub:join` | Client → Server | `{ roomId, meta? }` | Join a room. `meta` is optional user metadata broadcast to others. |
| `hub:joined` | Server → Client | `{ roomId, hubRole, userInfo }` | Confirmation that the client successfully joined. `userInfo` contains `{ fullname, avatar }`. |
| `hub:leave` | Client → Server | `{ roomId }` | Leave a room. |
| `hub:error` | Server → Client | `{ roomId?, error }` | Error response for any failed operation. |
| `hub:presence` | Server → Room | `{ event, roomId, user }` | Broadcast when a user joins or leaves. `event` is `"joined"` or `"left"`. `user` includes `{ id, fullname, avatar, hubRole }`. |

**Join a room:**

```js
socket.emit("hub:join", { roomId: "room-abc-123" });

socket.on("hub:joined", ({ roomId, hubRole, userInfo }) => {
  console.log("Joined room", roomId, "as", hubRole);
  // userInfo = { fullname: "John Doe", avatar: "https://..." }
});

socket.on("hub:presence", ({ event, roomId, user }) => {
  // user = { id, fullname, avatar, hubRole }
  if (event === "joined") showUserJoined(roomId, user);
  if (event === "left") showUserLeft(roomId, user);
});

socket.on("hub:error", ({ error }) => {
  console.error("Error:", error);
});
```

### Sending Messages

| Event | Direction | Payload |
|-------|-----------|---------|
| `hub:send` | Client → Server | `{ roomId, messageType, content, replyTo?, forwarded? }` |
| `hub:messageArrived` | Server → Room | `{ roomId, sender, message }` — `sender` includes `{ id, fullname, avatar }` |

**Send a message:**

```js
socket.emit("hub:send", {
  roomId: "room-abc-123",
  messageType: "text",
  content: { body: "Hello everyone!" }
});
```

**Receive messages:**

```js
socket.on("hub:messageArrived", ({ roomId, sender, message }) => {
  // sender = { id, fullname, avatar }
  // message = { id, messageType, content, timestamp, senderName, senderAvatar, ... }
  addMessageToUI(roomId, sender, message);
});
```

### History

When joining a room (if history is enabled), the server **automatically** sends the most recent messages right after the `hub:joined` event. Each message in the history array includes `senderName` and `senderAvatar` fields, so the frontend can render user display names and avatars without additional lookups.

| Event | Direction | Payload |
|-------|-----------|---------|
| `hub:history` | Server → Client | `{ roomId, messages[] }` |

**Automatic history on join:**

```js
socket.on("hub:history", ({ roomId, messages }) => {
  // messages are ordered newest-first, each has: id, roomId, senderId,
  // senderName, senderAvatar, messageType, content, timestamp, status
  renderMessageHistory(roomId, messages);
});
```

**Paginated history via REST (for "load more" / scroll-up):**

For older messages beyond the initial batch, use the REST endpoint:

```
GET /{hubKebabName}/{roomId}/messages?limit=50&offset=50
Authorization: Bearer {accessToken}
```

Response: `{ data: Message[], pagination: { total: number } }`

Each message in the REST response also contains `senderName` and `senderAvatar`.

### Custom Events

| Event | Direction | Payload |
|-------|-----------|---------|
| `hub:event` | Client → Server | `{ roomId, event, data }` |
| `hub:{eventName}` | Server → Room | `{ roomId, userId, ...data }` |

```js
// Emit a custom event
socket.emit("hub:event", {
  roomId: "room-abc-123",
  event: "customAction",
  data: { key: "value" }
});

// Listen for custom events
socket.on("hub:customAction", ({ roomId, userId, ...data }) => {
  handleCustomEvent(roomId, userId, data);
});
```

---

## Hub Definitions


## Hub: `gameHub`

**Namespace:** `/gameplay-api/hub/gameHub`
**Description:** Real-time chess game hub. Each chessGame is a room where two players exchange moves, see board state, and receive game lifecycle events (draw offer, resign, timeout, save requests). Supports both guest and registered players.

### Connection

```js
const gameHubSocket = io("{baseUrl}/gameplay-api/hub/gameHub", {
  path: "/gameplay-api/socket.io/",
  auth: { token: accessToken },
  transports: ["websocket", "polling"]
});
```

### Room Settings

| Setting | Value |
|---------|-------|
| Room DataObject | `chessGame` |
| Room Eligibility | `chessGame.status == 'active' || chessGame.status == 'pending' || chessGame.status == 'paused'` |
| Absolute Roles (bypass auth) | `administrator` |

**Authorization Sources** (checked in order, first match wins):

| # | Name | Source Object | User Field | Room Field | Hub Role | Condition |
|---|------|-----------|------------|------------|----------|-----------|
| 1 | `whitePlayer` | `chessGame` | `playerWhiteId` | `id` | `player` | — |
| 2 | `blackPlayer` | `chessGame` | `playerBlackId` | `id` | `player` | — |


### Hub Roles

| Role | Read | Send | Allowed Types | Moderated | Moderate | Delete Any | Manage Room |
|------|------|------|---------------|-----------|----------|------------|-------------|
| `player` | Yes | Yes | all | No | No | No | No |

Users with `absoluteRoles` get a built-in **system** role with all permissions.

**Room Eligibility Check:**
Before joining, you can check if a room supports real-time features:

```
GET /game-hub/{roomId}/eligible
```

Response: `{ "success": true, "eligible": true/false }`

Use this to conditionally show/hide the chat UI.

### Message Types

Messages are stored in the **`gameHubMessage`** DataObject with the following structure:

| Field | Type | Description |
|-------|------|-------------|
| `id` | ID | Primary key |
| `roomId` | ID | Reference to the room |
| `senderId` | ID | Reference to the sending user |
| `senderName` | String | Display name of the sender (denormalized at send time) |
| `senderAvatar` | String | Avatar URL of the sender (denormalized at send time) |
| `messageType` | Enum | One of: `text`, `system`, `chessMove`, `drawOffer`, `drawAccepted`, `drawDeclined`, `resignation`, `saveRequest`, `saveAccepted`, `saveDeclined`, `resumeRequest`, `resumeAccepted`, `resumeDeclined` |
| `content` | JSON | Type-specific payload (see below) |
| `timestamp` | DateTime | Message creation time |

#### Built-in Message Types

Each message type requires specific fields in the `content` object:

**`text`**

| Field | Type | Required |
|-------|------|----------|
| `body` | Text | Yes |

```js
socket.emit("hub:send", {
  roomId,
  messageType: "text",
  content: { body: "..." }
});
```

**`system`**

| Field | Type | Required |
|-------|------|----------|
| `systemAction` | String | Yes |
| `systemData` | JSON | No |

```js
socket.emit("hub:send", {
  roomId,
  messageType: "system",
  content: { systemAction: "..." }
});
```

#### Custom Message Types

**`chessMove`** — 

| Field | Type | Required |
|-------|------|----------|

```js
socket.emit("hub:send", {
  roomId,
  messageType: "chessMove",
  content: {  }
});
```

**`drawOffer`** — 

| Field | Type | Required |
|-------|------|----------|

```js
socket.emit("hub:send", {
  roomId,
  messageType: "drawOffer",
  content: {  }
});
```

**`drawAccepted`** — 

| Field | Type | Required |
|-------|------|----------|

```js
socket.emit("hub:send", {
  roomId,
  messageType: "drawAccepted",
  content: {  }
});
```

**`drawDeclined`** — 

| Field | Type | Required |
|-------|------|----------|

```js
socket.emit("hub:send", {
  roomId,
  messageType: "drawDeclined",
  content: {  }
});
```

**`resignation`** — 

| Field | Type | Required |
|-------|------|----------|

```js
socket.emit("hub:send", {
  roomId,
  messageType: "resignation",
  content: {  }
});
```

**`saveRequest`** — 

| Field | Type | Required |
|-------|------|----------|

```js
socket.emit("hub:send", {
  roomId,
  messageType: "saveRequest",
  content: {  }
});
```

**`saveAccepted`** — 

| Field | Type | Required |
|-------|------|----------|

```js
socket.emit("hub:send", {
  roomId,
  messageType: "saveAccepted",
  content: {  }
});
```

**`saveDeclined`** — 

| Field | Type | Required |
|-------|------|----------|

```js
socket.emit("hub:send", {
  roomId,
  messageType: "saveDeclined",
  content: {  }
});
```

**`resumeRequest`** — 

| Field | Type | Required |
|-------|------|----------|

```js
socket.emit("hub:send", {
  roomId,
  messageType: "resumeRequest",
  content: {  }
});
```

**`resumeAccepted`** — 

| Field | Type | Required |
|-------|------|----------|

```js
socket.emit("hub:send", {
  roomId,
  messageType: "resumeAccepted",
  content: {  }
});
```

**`resumeDeclined`** — 

| Field | Type | Required |
|-------|------|----------|

```js
socket.emit("hub:send", {
  roomId,
  messageType: "resumeDeclined",
  content: {  }
});
```

### Cross-cutting Features




No cross-cutting features (reply, forward, reaction) are enabled for this hub.

### Standard Events

| Event | Client Emits | Server Broadcasts |
|-------|-------------|-------------------|
| Presence Online | _(automatic on connect)_ | `hub:online` `{ roomId, userId }` |
| Presence Offline | _(automatic on disconnect)_ | `hub:offline` `{ roomId, userId }` |

**Example — Typing indicator:**

```js
// Start typing
socket.emit("hub:event", { roomId, event: "typing" });

// Stop typing (call after a debounce timeout)
socket.emit("hub:event", { roomId, event: "stopTyping" });

// Listen for others typing
socket.on("hub:typing", ({ roomId, userId }) => {
  showTypingIndicator(roomId, userId);
});
socket.on("hub:stopTyping", ({ roomId, userId }) => {
  hideTypingIndicator(roomId, userId);
});
```

### Custom Events

**`gameStateUpdate`** — Broadcast updated game state (status, result, timers) to both players when the game state changes server-side.
Direction: `serverToRoom`

```js
// Emit
socket.emit("hub:event", {
  roomId,
  event: "gameStateUpdate",
  data: { /* your payload */ }
});

// Listen
socket.on("hub:gameStateUpdate", ({ roomId, userId, ...data }) => {
  // handle event
});
```

**`clockTick`** — Server broadcasts remaining time for each player (for timed/blitz/rapid games).
Direction: `serverToRoom`

```js
// Emit
socket.emit("hub:event", {
  roomId,
  event: "clockTick",
  data: { /* your payload */ }
});

// Listen
socket.on("hub:clockTick", ({ roomId, userId, ...data }) => {
  // handle event
});
```

### Auto-Bridged Server Events

These events are **automatically** emitted to rooms when DataObject changes occur on the backend (via Kafka). The frontend only needs to **listen**:

| Event | Trigger | Payload |
|-------|---------|---------|
| `hub:messageEdited` | `gameHubMessage` updated | DataObject record fields |
| `hub:messageDeleted` | `gameHubMessage` deleted | DataObject record fields |
| `hub:roomUpdated` | `chessGame` updated | DataObject record fields |
| `hub:roomClosed` | `chessGame` deleted | DataObject record fields |

```js
socket.on("hub:messageEdited", ({ roomId, ...data }) => {
  handleMessageEdited(roomId, data);
});
socket.on("hub:messageDeleted", ({ roomId, ...data }) => {
  handleMessageDeleted(roomId, data);
});
socket.on("hub:roomUpdated", ({ roomId, ...data }) => {
  handleRoomUpdated(roomId, data);
});
socket.on("hub:roomClosed", ({ roomId, ...data }) => {
  handleRoomClosed(roomId, data);
});
```

### Moderation Commands

Users with `canModerate` permission can block, silence, and manage messages:

**Block/Unblock a user:**

```js
socket.emit("hub:block", { roomId, userId: targetUserId, reason: "Spam", duration: 3600 });
socket.emit("hub:unblock", { roomId, userId: targetUserId });
```

**Silence/Unsilence a user:**

```js
socket.emit("hub:silence", { roomId, userId: targetUserId, reason: "Off-topic", duration: 600 });
socket.emit("hub:unsilence", { roomId, userId: targetUserId });
```

Duration is in seconds. `0` or omitted = permanent.

**Listen for moderation actions on your user:**

```js
socket.on("hub:blocked", ({ roomId, reason }) => {
  // You have been blocked — leave UI, show message
});
socket.on("hub:unblocked", ({ roomId }) => {
  // Block lifted — you may rejoin the room
});
socket.on("hub:silenced", ({ roomId, reason }) => {
  // You have been silenced — disable send button
});
socket.on("hub:unsilenced", ({ roomId }) => {
  // Silence lifted — re-enable send button
});
```



### REST API Endpoints

In addition to Socket.IO, the hub exposes REST endpoints for message history and management:

**Get message history:**

```
GET /game-hub/{roomId}/messages?limit=50&offset=0
```

Response:
```json
{
  "success": true,
  "data": [ /* message objects */ ],
  "pagination": { "limit": 50, "offset": 0, "total": 120 }
}
```

**Send a message via REST:**

```
POST /game-hub/{roomId}/messages
Content-Type: application/json
Authorization: Bearer {accessToken}

{
  "data": { "body": "Hello from REST" },
  "replyTo": null
}
```

Messages sent via REST are also broadcast to all connected Socket.IO clients in the room.

**Delete a message:**

```
DELETE /game-hub/{roomId}/messages/{messageId}
Authorization: Bearer {accessToken}
```

### Guardrails

| Limit | Value |
|-------|-------|
| Max users per room | 3 |
| Max rooms per user | 5 |
| Message rate limit | 120 msg/min |
| Max message size | 16384 bytes |
| Connection timeout | 600000 ms |
| History on join | Last 100 messages |


## Frontend Integration Checklist

1. **Install socket.io-client:** `npm install socket.io-client`
2. **Create a connection manager** that handles connect/disconnect/reconnect with token refresh.
3. **Join rooms** after connection. Listen for `hub:joined` before sending messages. The `hub:joined` event includes the user's `hubRole` and `userInfo` (fullname, avatar).
4. **Render chat history** from the `hub:history` event that arrives automatically after joining. Each message includes `senderName` and `senderAvatar` for display.
5. **Handle `hub:error`** globally for all error responses.
6. **Use sender info** from `hub:messageArrived` events — the `sender` object includes `{ id, fullname, avatar }`. For history messages, use the stored `senderName` and `senderAvatar` fields.
7. **Parse `messageType`** to render different message bubbles (text, image, video, etc.).
8. **Use REST endpoints** for paginated history when scrolling up in a conversation (`GET /{hubKebabName}/{roomId}/messages?limit=50&offset=50`).
9. **Debounce typing indicators** — emit `typing` on keypress, `stopTyping` after 2–3 seconds of inactivity.
10. **Track read receipts** per room to show unread counts and read status.
11. **Handle presence** to show online/offline status. The `hub:presence` event includes `user.fullname` and `user.avatar` for display.
12. **Reconnect gracefully** — re-join rooms and fetch missed messages via REST on reconnect.

## Example: Full Chat Integration

```js
import { io } from "socket.io-client";

class ChatHub {
  constructor(baseUrl, token) {
    this.socket = io(`${baseUrl}/gameplay-api/hub/gameHub`, {
      path: "/gameplay-api/socket.io/",
      auth: { token },
      transports: ["websocket", "polling"]
    });

    this.rooms = new Map();
    this._setupListeners();
  }

  _setupListeners() {
    this.socket.on("hub:joined", ({ roomId, hubRole, userInfo }) => {
      this.rooms.set(roomId, { joined: true, hubRole, userInfo, messages: [], members: new Map() });
    });

    this.socket.on("hub:history", ({ roomId, messages }) => {
      const room = this.rooms.get(roomId);
      if (room) room.messages = messages;
      // Each message has senderName and senderAvatar for display
    });

    this.socket.on("hub:presence", ({ event, roomId, user }) => {
      const room = this.rooms.get(roomId);
      if (!room) return;
      if (event === "joined") {
        room.members.set(user.id, { fullname: user.fullname, avatar: user.avatar, hubRole: user.hubRole });
      } else if (event === "left") {
        room.members.delete(user.id);
      }
    });

    this.socket.on("hub:messageArrived", ({ roomId, sender, message }) => {
      // sender = { id, fullname, avatar }
      const room = this.rooms.get(roomId);
      if (room) room.messages.push(message);
      this.onNewMessage?.(roomId, sender, message);
    });

    this.socket.on("hub:error", ({ error }) => {
      console.error("[ChatHub]", error);
    });

    this.socket.on("hub:online", ({ roomId, userId }) => {
      this.onPresence?.(userId, "online");
    });
    this.socket.on("hub:offline", ({ roomId, userId }) => {
      this.onPresence?.(userId, "offline");
    });
  }

  joinRoom(roomId) {
    this.socket.emit("hub:join", { roomId });
  }

  leaveRoom(roomId) {
    this.socket.emit("hub:leave", { roomId });
    this.rooms.delete(roomId);
  }

  sendMessage(roomId, messageType, content, options = {}) {
    this.socket.emit("hub:send", {
      roomId,
      messageType,
      content,
      ...options
    });
  }

  disconnect() {
    this.socket.disconnect();
  }
}
```

**After this prompt, the user may give you new instructions to update the output of this prompt or provide subsequent prompts about the project.**
