This document describes all WebSocket events used in the Poker Engine for real-time communication between clients and the server.
Server: Socket.io v4.x
Total Events: 69 (20 Client→Server, 49 Server→Client). The canonical types are ClientToServerEvents and ServerToClientEvents in src/websocket/types/events.ts; client-side copies live in packages/shared/src/types/events.ts.
Table of Contents
- Connection Setup
- Authentication
- Client → Server Events
- Server → Client Events
- Room Management
- State Synchronization
- Error Handling
- Reconnection
Connection Setup
Connecting to the Server
import { io } from 'socket.io-client';
const socket = io('wss://your-server.com', {
auth: {
token: 'your-jwt-access-token'
},
transports: ['websocket'],
reconnection: true,
reconnectionAttempts: 5,
reconnectionDelay: 1000
});
Configuration
| Setting | Value | Description |
|---|---|---|
| Ping Timeout | 60,000ms | Time before connection considered dead |
| Ping Interval | 25,000ms | Heartbeat frequency |
| Reconnection Window | 300,000ms default | Fallback; the admin-configured reconnectWindowSeconds takes precedence |
| CORS Origins | CORS_ORIGINS | Falls back to localhost:3000 and localhost:5173 |
Authentication
The server reads a JWT from the auth.token handshake parameter, then falls back to the HttpOnly access_token cookie.
Connections with no token join as guests with lobby-only access, limited to 5 per IP address. Guests have an ID of the form guest-<uuid> and socket.data.isGuest set to true.
Token Validation
The server validates the JWT token on connection and extracts:
interface SocketData {
userId: string;
username: string;
email: string;
groupId: string;
permissions: string;
currentTableId?: string;
connectedAt: number;
lastActivity: number;
sessionId: string;
adminSessionId?: string;
isGuest?: boolean;
isSpectator?: boolean;
spectatingTableId?: string;
intentionalLogout?: boolean;
}
Connection Established
Upon successful authentication, the server emits:
socket.on('connection_established', (data) => {
console.log('Connected:', data);
// { userId: 'uuid', sessionId: 'uuid' }
});
Authentication Failure
If authentication fails, the connection is rejected with an error:
socket.on('connect_error', (error) => {
console.error('Connection failed:', error.message);
// 'Authentication failed: Invalid or expired token'
// 'Too many connections from this IP address' (guests)
});
Client → Server Events
Game Events
join_table
Join a poker table and take a seat.
Payload:
{
tableId: string; // Table UUID
seatIndex: number; // Seat position (0-9)
buyIn: number; // Chip amount to buy in
password?: string; // Required for private tables
}
Callback Response:
{
success: boolean;
error?: string; // Error message if failed
tableState?: TableState;
}
Example:
socket.emit('join_table', {
tableId: 'abc123',
seatIndex: 3,
buyIn: 1000
}, (response) => {
if (response.success) {
console.log('Joined table successfully');
} else {
console.error('Failed to join:', response.error);
}
});
leave_table
Leave the current table and cash out.
Payload:
{
tableId: string;
}
Callback Response:
{
success: boolean;
error?: string;
cashOut?: number; // Final chip amount returned
}
Example:
socket.emit('leave_table', { tableId: 'abc123' }, (response) => {
if (response.success) {
console.log('Cashed out:', response.cashOut);
}
});
player_action
Submit a betting action during your turn.
Payload:
{
action: 'fold' | 'check' | 'call' | 'bet' | 'raise' | 'all-in';
amount?: number; // Required for bet/raise
// Optional prompt context from the latest action_required payload
handNumber?: number;
street?: string | null;
bettingRound?: string | null;
idempotencyKey?: string; // Max 128 characters
}
Callback Response:
{
success: boolean;
error?: string;
errorCode?: string; // e.g. STALE_HAND, STALE_STREET, DUPLICATE_ACTION
cached?: boolean; // True if an idempotent retry returned the cached result
}
Example:
// Fold
socket.emit('player_action', { action: 'fold' }, callback);
// Call
socket.emit('player_action', { action: 'call' }, callback);
// Raise to 100
socket.emit('player_action', { action: 'raise', amount: 100 }, callback);
// All-in
socket.emit('player_action', { action: 'all-in' }, callback);
Offer the actions in validActions from the latest action_required payload.
sit_out
Temporarily sit out from the game (skip hands).
Payload:
{
tableId: string;
}
Example:
socket.emit('sit_out', { tableId: 'abc123' });
sit_in
Return to active play after sitting out.
Payload:
{
tableId: string;
}
Example:
socket.emit('sit_in', { tableId: 'abc123' });
add_chips
Add more chips to your stack (rebuy).
Payload:
{
tableId: string;
amount: number;
}
Callback Response:
{
success: boolean;
error?: string;
errorCode?: string;
}
Constraints:
- Total stack cannot exceed table’s max buy-in
chat_message
Send a chat message to the table.
Payload:
{
tableId: string;
message: string; // Schema max 500 characters; stored message truncated to 200
}
Example:
socket.emit('chat_message', {
tableId: 'abc123',
message: 'Nice hand!'
});
use_time_bank
Spend time-bank seconds on your current action.
Payload:
{
tableId: string;
seconds?: number; // Positive integer, max 600
}
Callback Response:
{
success: boolean;
error?: string;
secondsUsed?: number;
remainingSeconds?: number;
actionTimeRemainingMs?: number | null;
}
logout_intent
Tell the server the next disconnect is an intentional logout.
Payload: None. Callback Response: { success: boolean; error?: string }
request_table_state
Request the current table state for a table you are seated at. Requires a seat at that table.
Payload: { tableId: string }
Callback Response: { success: boolean; error?: string; sequenceNumber?: number }
ack_delivery
Acknowledge a guaranteed-delivery event (see Guaranteed Delivery).
Payload: { deliveryId: string } (max 64 characters)
Callback Response: { success: boolean; error?: string }
watch_table / stop_watching
Start or stop spectating a table.
Payload: { tableId: string }
Callback Response (watch_table): { success: boolean; error?: string; tableState?: TableState; sequenceNumber?: number }
Callback Response (stop_watching): { success: boolean; error?: string }
tournament_rebuy / tournament_addon / tournament_reentry
Tournament chip actions for the player’s current table.
Payload:
{
tableId: string;
idempotencyKey?: string; // 1-128 characters
}
Callback Response:
{
success: boolean;
error?: string;
errorCode?: string;
queued?: boolean;
replayed?: boolean;
}
ping
Heartbeat for latency measurement.
Payload: None
Callback Response:
{
pong: boolean;
timestamp: number;
}
Example:
const start = Date.now();
socket.emit('ping', (response) => {
const latency = Date.now() - start;
console.log('Latency:', latency, 'ms');
});
Lobby Events
subscribe_lobby
Subscribe to real-time lobby updates.
Payload: None
Example:
socket.emit('subscribe_lobby');
After subscribing, you’ll receive:
lobby_state– Initial full statelobby_update/lobby_updates– Real-time changes
unsubscribe_lobby
Stop receiving lobby updates.
Payload: None
Example:
socket.emit('unsubscribe_lobby');
request_lobby_state
Request immediate lobby state refresh.
Payload: None
Example:
socket.emit('request_lobby_state');
// Server responds with 'lobby_state' event
Server → Client Events
Connection Events
connection_established
Confirms successful authentication.
Payload:
{
userId: string;
sessionId: string;
}
reconnected
Player rejoined table after disconnect.
Payload:
{
tableId: string;
previousTableId?: string;
tableMove?: {
fromTableId: string;
toTableId: string;
reason: 'reconnect_after_table_move';
};
state: TableState | { message: string };
sequenceGap?: {
hasMissedEvents: boolean;
lastReceivedSequence: number;
currentSequence: number;
missedCount: number;
};
_seq?: number;
_ts?: number;
}
error
Error notification.
Payload:
{
code: string;
message: string;
}
Error Codes:
| Code | Description |
|---|---|
JOIN_FAILED | Unable to join table |
LEAVE_FAILED | Unable to leave table |
ACTION_FAILED | Invalid or rejected action |
ADD_CHIPS_FAILED | Cannot add chips |
TABLE_NOT_FOUND | Table doesn’t exist |
TIME_BANK_FAILED | Cannot use time bank |
WATCH_FAILED | Cannot watch table |
STOP_WATCHING_FAILED | Cannot stop watching |
CHAT_DISABLED | Chat is off |
CHAT_FORBIDDEN | Not allowed to chat |
PLAYER_PROTECTION | Blocked by account protection |
ACCOUNT_CHECK_FAILED | Account state could not be checked |
DUPLICATE_SESSION | Session already active elsewhere |
MAINTENANCE | Maintenance mode is on |
SESSION_EXPIRED | Session revoked or expired |
REFUND_FAILED | Buy-in refund failed |
RATE_LIMIT | Too many requests |
server_migrating
The server is draining for a live session migration.
Payload:
{
resumeExpectedMs: number;
reason: 'redeploy' | 'operator' | 'sigusr2';
drainDeadlineAt: string | null;
}
hand_voided
A hand was voided after a server failure.
Payload: { tableId: string; handNumber?: number; reason: 'server_failure'; message: string }
Table State Events
table_state
Complete or delta table state update.
Payload:
interface TableState {
id: string;
name: string;
mode: string;
type: string;
tournamentId?: string | null;
gameType?: string;
maxPlayers: number;
minBuyIn: number;
maxBuyIn: number;
smallBlind: number;
bigBlind: number;
ante: number;
gameState: string;
isPaused: boolean;
handNumber: number;
dealerSeat: number;
smallBlindSeat: number;
bigBlindSeat: number;
currentPlayerSeat: number;
bettingRound?: string | null;
street?: string | null;
communityCards: string[];
pot: number;
currentBet: number;
players: TablePlayer[];
actionTimeRemaining: number | null;
bettingLimit?: string;
minRaise?: number;
showdownDelayMs?: number;
_seq?: number; // Sequence number
_ts?: number; // Server timestamp
// Also: ruleProfile, tournamentRules, handForHand, buttonBlind, bringIn,
// completionBet, smallBet, bigBet, bringInSeat, isBettingRound, mixedGame
}
interface TablePlayer {
id: string;
username?: string;
stack: number;
seatIndex: number;
isSittingOut: boolean;
hasFolded: boolean;
isAllIn: boolean;
currentBet: number;
totalBet: number;
holeCards: string[] | null;
studCards?: StudVisibleCard[] | null;
hasActed: boolean;
timeBankRemaining: number;
isBot: boolean;
avatar?: string;
isWaitingForNextHand?: boolean;
isBringIn?: boolean;
}
Card Visibility:
- State is filtered per viewer, so opponents’ hole cards are hidden
- At showdown: non-mucked cards are revealed
player_joined
New player seated at table.
Payload:
{
userId: string;
username: string;
seatIndex: number;
stack: number;
}
player_left
Player left the table.
Payload:
{
userId: string;
}
game_started
A new hand has begun.
Payload:
{
handNumber: number;
dealerSeat: number;
}
hole_cards
Your private hole cards (sent only to you).
Payload:
{
cards: string[]; // Count depends on variant, e.g. ['AS', 'KH']
tableId: string;
handNumber: number;
gameType: string;
street: string | null;
bettingRound: string | null;
_seq?: number;
_ts?: number;
_deliveryId?: string;
_isRetry?: boolean;
}
Card Format:
- Rank: 2-9, T (10), J, Q, K, A
- Suit: S (Spades), H (Hearts), D (Diamonds), C (Clubs)
- Examples:
AS(Ace of Spades),TH(Ten of Hearts)
action_required
It’s your turn to act.
Payload:
{
tableId: string;
userId: string;
timeout: number;
timeBankRemaining?: number;
timeBankEnabled?: boolean;
timeBankIncrementSeconds?: number;
timeoutPolicy?: string;
validActions: string[]; // ['fold', 'call', 'raise']
callAmount?: number;
minBet?: number;
maxBet?: number;
minRaise?: number;
maxRaise?: number;
bettingLimit?: string;
handNumber: number;
gameType: string;
street: string | null;
bettingRound: string | null;
}
Example:
socket.on('action_required', (data) => {
if (data.userId === myUserId) {
showActionButtons(data.validActions);
startTimer(data.timeout);
}
});
player_acted
A player made an action (broadcast to all).
Payload:
{
userId: string;
action: string;
amount?: number;
tableId: string;
handNumber: number;
gameType: string;
street: string | null;
bettingRound: string | null;
}
community_cards
Community cards revealed.
Payload:
{
cards: string[];
allCards?: string[];
stage: string;
previousBets?: Array<{ playerId: string; amount: number }>;
tableId: string;
handNumber: number;
gameType: string;
street: string | null;
bettingRound: string | null;
}
Example:
pot_update
Pot amount changed.
Payload:
{
pot: number;
sidePots?: Array<{
amount: number;
eligiblePlayers: string[];
}>;
idempotencyKey?: string;
_deliveryId?: string;
_isRetry?: boolean;
}
showdown
Cards revealed at showdown.
Payload:
{
players: Array<{
playerId: string;
cards: string[];
handName: string;
handRank?: number;
bestHandCards?: string[];
wasMucked?: boolean;
}>;
showOrder?: string[];
allInPlayerIds?: string[];
muckedPlayerIds?: string[];
hiLo?: ShowdownHiLoPayload;
muckedHandRevealDelayMs?: number;
revealTiming?: 'all_in_before_runout' | 'final_showdown';
tableId: string;
handNumber: number;
gameType: string;
street: string | null;
bettingRound: string | null;
_deliveryId?: string;
_isRetry?: boolean;
}
hand_result
Final result of the hand.
Payload:
{
tableId: string;
handNumber: number;
gameType: string;
street: string | null;
bettingRound: string | null;
winners: Array<{
playerId: string;
amount: number;
hand?: string; // Winning hand description
handName?: string;
winType?: 'high' | 'low';
bestCards?: string[];
isUncalledReturn?: boolean;
}>;
showOrder?: string[];
allInPlayerIds?: string[];
muckedPlayerIds?: string[];
hiLo?: ShowdownHiLoPayload;
muckedHandRevealDelayMs?: number;
idempotencyKey?: string;
_deliveryId?: string;
_isRetry?: boolean;
}
Example:
socket.on('hand_result', (data) => {
data.winners.forEach(winner => {
console.log(`${winner.playerId} won ${winner.amount} with ${winner.hand}`);
});
});
Other hand events
| Event | Payload |
|---|---|
time_bank_used | playerId, secondsUsed, remainingSeconds, actionTimeRemainingMs, policy?, plus hand context |
deck_procedure | procedure (deck/deal correction or fatal block), plus hand context |
betting_started | activePlayerId, activePlayerSeat, currentBet, pot, plus hand context |
forced_bets | bets[] (type, playerId, amount, seatIndex, isAllIn?, remainingStack?), pot, total, plus hand context |
blinds_posted | smallBlind?, bigBlind?, buttonBlind?, antes?, plus hand context |
player_eliminated | userId, tableId, canAddChips, minBuyIn, maxBuyIn, tournamentId?, timeoutSeconds?, rebuy?, reentry? |
player_away / player_active | userId |
Hand context is tableId, handNumber, gameType, street, bettingRound.
Chat Events
chat_message
Chat message received at table.
Payload:
{
userId: string;
username: string;
message: string;
timestamp: number;
}
chat_history
Recent chat for the table. Payload: { messages: ChatMessagePayload[] }
Lobby Events
lobby_state
Full lobby state (sent on subscribe).
Payload:
{
tables: TableInfo[];
totalPlayers: number;
timestamp: number;
}
interface TableInfo {
id: string;
name: string;
mode: string;
type: string;
maxPlayers: number;
playerCount: number;
smallBlind: number;
bigBlind: number;
minBuyIn: number;
maxBuyIn: number;
isPrivate: boolean;
gameState: string;
gameType: string;
bettingLimit?: string;
ruleProfile?: RuleProfileSummary;
}
lobby_update
Single table change.
Payload:
{
type: LobbyEventType;
tableId: string;
table?: TableInfo;
timestamp: number;
}
LobbyEventType Values:
| Type | Description |
|---|---|
table_created | New public table appeared |
table_removed | Table closed/removed |
table_updated | Table configuration changed |
player_joined | Player sat at table |
player_left | Player left table |
player_sat_out | Player sat out |
player_sat_in | Player sat in |
game_started | Hand began |
game_ended | Hand completed |
lobby_updates
Batched lobby updates (debounced 100ms).
Payload:
Array<{
type: LobbyEventType;
tableId: string;
table?: TableInfo;
timestamp: number;
}>
lobby_stats
Lobby statistics update.
Payload:
{
totalTables: number;
totalPlayers: number;
activeTables: number;
timestamp: number;
}
Admin Events
table_settings_updated
Table settings changed by admin (broadcast to all players at the table).
Payload:
{
tableId: string;
name: string;
smallBlind: number;
bigBlind: number;
minBuyIn: number;
maxBuyIn: number;
rake: number;
maxPlayers: number;
tableType?: string;
currency?: string;
styleId?: string | null;
moveTimeSeconds?: number | null;
showdownDelaySeconds?: number | null;
}
Example:
socket.on('table_settings_updated', (data) => {
console.log('Table settings updated:', data.name);
updateTableSettings(data);
});
Account and Platform Events
| Event | Payload | Notes |
|---|---|---|
balance_update | balance, virtualBalance, tournamentTickets? | |
account_updated | reason | Refetch the current user (GET /auth/me); reason is informational |
force_disconnect | reason | Server is closing this session |
reality_check | elapsedMinutes, netResult, handsPlayed | Responsible-gaming reminder |
maintenance_status | enabled, message? | |
feature_toggles_updated | toggles, changed, timestamp | |
tier_updated | allowedByTier, tierName, variationId, featuresIncluded, featuresTotal, disabledFeatures, timestamp | License tier changed |
wagering_progress | userId, totalRequired, totalCompleted, totalRemaining, percentComplete, hasActiveWagering, bonusCredits, updatedAt |
Tournament Events
| Event | Payload |
|---|---|
tournament_registration_update | tournamentId, registeredCount, maxPlayers |
tournament_starting | tournamentId, startsIn |
tournament_started | tournamentId, tableId, tableAssignments? |
tournament_paused | tournamentId, pausedAt, pausedBy, timeRemaining, currentLevel, reason? |
tournament_resumed | tournamentId, resumedAt, resumedBy, currentLevel, timeRemaining |
tournament_break_pending | tournamentId, requestedBy, requestedAt, durationSeconds, reason?, pendingTableIds |
tournament_floor_message | tournamentId, operation, correlationId, message, severity, actorId, reason, affectedPlayerIds, affectedTableIds, createdAt |
tournament_table_move | tournamentId, playerId, fromTableId, fromSeatIndex, toTableId, toSeatIndex, newSeatIndex, reason |
tournament_final_table | tournamentId, finalTableId, playerCount |
tournament_blind_change | tournamentId, level, smallBlind, bigBlind, ante, nextLevelIn, effectiveAt?, appliedTableIds?, pendingTableIds?, policy? |
Room Management
Table Rooms
Players at a table join table:{tableId}:players; spectators join table:{tableId}:spectators. broadcastToTable delivers to both.
- Auto-join: When you emit
join_tableorwatch_tablesuccessfully - Auto-leave: When you emit
leave_tableorstop_watching
User Rooms
Each user also has user:{userId}, used for private delivery such as sendToUser.
Lobby Room
Global room for lobby subscribers: lobby
- Join: Emit
subscribe_lobby - Leave: Emit
unsubscribe_lobby - Only receives updates about PUBLIC tables
State Synchronization
Sequence Numbers
Events broadcast with broadcastToTable carry _seq (per-table sequence number) and _ts (server timestamp) for ordering:
let lastSeq = 0;
socket.on('table_state', (state) => {
if (state._seq !== undefined && state._seq <= lastSeq) {
// Outdated update, ignore
return;
}
lastSeq = state._seq ?? lastSeq;
updateUI(state);
});
Resynchronizing
table_state is always a complete snapshot; there are no delta updates. Clients that detect a gap (reconnected.sequenceGap) can emit request_table_state.
Guaranteed Delivery
hole_cards, action_required, hand_result, pot_update, showdown, game_started, forced_bets and player_acted may carry a _deliveryId. Respond with ack_delivery. Unacknowledged deliveries are retried up to 3 times, 5 seconds apart, and retries carry _isRetry: true.
Error Handling
Connection Errors
socket.on('connect_error', (error) => {
if (error.message.startsWith('Authentication failed')) {
refreshToken().then(() => socket.connect());
}
});
Game Errors
socket.on('error', (error) => {
switch (error.code) {
case 'JOIN_FAILED':
showError('Could not join table: ' + error.message);
break;
case 'ACTION_FAILED':
showError('Invalid action: ' + error.message);
break;
// Handle other errors...
}
});
Reconnection
Automatic Reconnection
Socket.io handles reconnection automatically. The React client uses:
const socket = io(url, {
reconnection: true,
reconnectionAttempts: Infinity,
reconnectionDelay: 1000,
reconnectionDelayMax: 10000
});
Session Recovery
If disconnected while at a table, you can reconnect within the admin-configured reconnect window (reconnectWindowSeconds):
- Reconnect with same token
- Server recognizes your session
reconnectedevent sent with current table state- You’re back in the game at your seat
socket.on('reconnected', (data) => {
console.log('Rejoined table:', data.tableId);
updateTableState(data.state);
});
Session Timeout
A disconnect also starts a removal timer using the admin-configured disconnect grace period; when it fires, the player is removed from the table.
Client Implementation Example
import { io } from 'socket.io-client';
class PokerClient {
constructor(serverUrl, token) {
this.socket = io(serverUrl, {
auth: { token }
});
this.setupEventHandlers();
}
setupEventHandlers() {
// Connection
this.socket.on('connection_established', this.onConnected.bind(this));
this.socket.on('connect_error', this.onConnectError.bind(this));
this.socket.on('reconnected', this.onReconnected.bind(this));
// Game state
this.socket.on('table_state', this.onTableState.bind(this));
this.socket.on('hole_cards', this.onHoleCards.bind(this));
this.socket.on('community_cards', this.onCommunityCards.bind(this));
this.socket.on('action_required', this.onActionRequired.bind(this));
this.socket.on('player_acted', this.onPlayerActed.bind(this));
this.socket.on('hand_result', this.onHandResult.bind(this));
// Players
this.socket.on('player_joined', this.onPlayerJoined.bind(this));
this.socket.on('player_left', this.onPlayerLeft.bind(this));
// Chat
this.socket.on('chat_message', this.onChatMessage.bind(this));
// Errors
this.socket.on('error', this.onError.bind(this));
}
// Actions
joinTable(tableId, seatIndex, buyIn, password) {
return new Promise((resolve, reject) => {
this.socket.emit('join_table',
{ tableId, seatIndex, buyIn, password },
(response) => {
if (response.success) resolve();
else reject(new Error(response.error));
}
);
});
}
leaveTable(tableId) {
return new Promise((resolve, reject) => {
this.socket.emit('leave_table', { tableId }, (response) => {
if (response.success) resolve(response.cashOut);
else reject(new Error(response.error));
});
});
}
act(action, amount) {
return new Promise((resolve, reject) => {
this.socket.emit('player_action', { action, amount }, (response) => {
if (response.success) resolve();
else reject(new Error(response.error));
});
});
}
sendChat(tableId, message) {
this.socket.emit('chat_message', { tableId, message });
}
subscribeLobby() {
this.socket.emit('subscribe_lobby');
}
unsubscribeLobby() {
this.socket.emit('unsubscribe_lobby');
}
// Event handlers (implement in your app)
onConnected(data) { /* ... */ }
onConnectError(error) { /* ... */ }
onReconnected(data) { /* ... */ }
onTableState(state) { /* ... */ }
onHoleCards(data) { /* ... */ }
onCommunityCards(data) { /* ... */ }
onActionRequired(data) { /* ... */ }
onPlayerActed(data) { /* ... */ }
onHandResult(data) { /* ... */ }
onPlayerJoined(data) { /* ... */ }
onPlayerLeft(data) { /* ... */ }
onChatMessage(data) { /* ... */ }
onError(error) { /* ... */ }
}
Security Notes
- JWT or Guest – Connections authenticate with a valid JWT, or join as rate-limited guests with lobby-only access
- Server-Side Validation – All actions validated server-side
- Card Security – Table state is filtered per viewer; opponent cards are hidden until showdown
- Rate Limiting – Per-user limits apply, e.g.
player_action2/second andchat_message3/6 seconds - Message Sanitization – Chat messages are stripped of HTML, filtered, and truncated to 200 characters
- Token Expiration – Expired tokens rejected on connection
React Client Implementation
The React client uses custom hooks for WebSocket management.
Available Hooks
| Hook | Purpose | Location |
|---|---|---|
useSocket | Base socket connection management | client/src/hooks/useSocket.ts |
useGameSocket | Game table events and actions | client/src/hooks/useGameSocket.ts |
useLobbySocket | Lobby subscription and updates | client/src/hooks/useLobbySocket.ts |
useSpectatorSocket | Watching a table as a spectator | client/src/hooks/useSpectatorSocket.ts |
useSocket
Base hook for socket connection lifecycle:
import { useSocket } from '@/hooks';
function MyComponent() {
const { socket, isConnected, userId, sessionId, error, connect, disconnect } = useSocket();
// Socket connects automatically when user is authenticated
// Disconnects on logout or unmount
}
useGameSocket
Game-specific hook with table actions:
import { useGameSocket } from '@/hooks';
function TablePage({ tableId }) {
const {
isConnected,
isJoined,
error,
joinTable,
leaveTable,
sendAction,
sitOut,
sitIn,
useTimeBank,
addChips,
tournamentRebuy,
tournamentAddOn,
tournamentReentry,
sendChat,
} = useGameSocket({ tableId });
// Join table
const handleJoin = async (seatIndex: number, buyIn: number) => {
const success = await joinTable(seatIndex, buyIn);
};
// Send action
const handleAction = async (action: string, amount?: number) => {
await sendAction(action, amount);
};
}
useLobbySocket
Lobby subscription hook:
import { useLobbySocket } from '@/hooks';
function LobbyPage() {
const { isConnected, isSubscribed, subscribe, unsubscribe, refreshLobby } = useLobbySocket();
// Automatically subscribes to lobby updates
// Updates lobbyStore with real-time table changes
}
Socket Singleton
The socket connection is managed as a singleton in client/src/lib/socket.ts:
import { connectSocket, disconnectSocket, getSocket } from '@/lib/socket';
// Connect (called automatically by useSocket)
const socket = connectSocket();
// Get existing socket
const socket = getSocket();
// Disconnect
disconnectSocket();
Development Proxy
In development, the Vite dev server proxies WebSocket connections to avoid CORS issues:
// vite.config.ts
server: {
proxy: {
'/socket.io': {
target: 'http://localhost:3000',
ws: true,
},
},
}
The client automatically uses the proxy in development mode.