518 lines
11 KiB
Markdown
518 lines
11 KiB
Markdown
|
|
# @lilith/websocket-client
|
||
|
|
|
||
|
|
WebSocket client library with React hooks for real-time features in the lilith-platform.
|
||
|
|
|
||
|
|
## Features
|
||
|
|
|
||
|
|
- **Type-safe WebSocket client** - Full TypeScript support with typed events
|
||
|
|
- **Auto-reconnection** - Exponential backoff retry strategy
|
||
|
|
- **JWT authentication** - Secure token-based authentication
|
||
|
|
- **React hooks** - Easy-to-use hooks for common real-time features
|
||
|
|
- **Event subscriptions** - Menu, Goal, Tip, and Chatbot events
|
||
|
|
- **Connection state management** - Track connection status and errors
|
||
|
|
|
||
|
|
## Installation
|
||
|
|
|
||
|
|
```bash
|
||
|
|
pnpm add @lilith/websocket-client
|
||
|
|
```
|
||
|
|
|
||
|
|
## Quick Start
|
||
|
|
|
||
|
|
### Basic Connection
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
import { useWebSocket } from '@lilith/websocket-client';
|
||
|
|
|
||
|
|
function App() {
|
||
|
|
const { socket, connected, error } = useWebSocket({
|
||
|
|
url: 'ws://localhost:4001',
|
||
|
|
token: 'your-jwt-token',
|
||
|
|
});
|
||
|
|
|
||
|
|
if (error) return <div>Error: {error.message}</div>;
|
||
|
|
if (!connected) return <div>Connecting...</div>;
|
||
|
|
|
||
|
|
return <div>Connected!</div>;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Hooks
|
||
|
|
|
||
|
|
### useWebSocket
|
||
|
|
|
||
|
|
Main connection hook. Manages WebSocket lifecycle.
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
const { socket, client, connected, connecting, error } = useWebSocket({
|
||
|
|
url: 'ws://localhost:4001',
|
||
|
|
token: userToken,
|
||
|
|
reconnection: true,
|
||
|
|
reconnectionAttempts: Infinity,
|
||
|
|
reconnectionDelay: 1000,
|
||
|
|
reconnectionDelayMax: 5000,
|
||
|
|
autoConnect: true,
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
**Parameters:**
|
||
|
|
- `url` (string, required) - WebSocket server URL
|
||
|
|
- `token` (string, optional) - JWT authentication token
|
||
|
|
- `reconnection` (boolean, default: true) - Enable auto-reconnection
|
||
|
|
- `reconnectionAttempts` (number, default: Infinity) - Max reconnection attempts
|
||
|
|
- `reconnectionDelay` (number, default: 1000) - Initial reconnection delay (ms)
|
||
|
|
- `reconnectionDelayMax` (number, default: 5000) - Max reconnection delay (ms)
|
||
|
|
- `autoConnect` (boolean, default: true) - Connect automatically on mount
|
||
|
|
|
||
|
|
**Returns:**
|
||
|
|
- `socket` (Socket | null) - Socket.IO socket instance
|
||
|
|
- `client` (WebSocketClient | null) - Client wrapper instance
|
||
|
|
- `connected` (boolean) - Connection status
|
||
|
|
- `connecting` (boolean) - Connection in progress
|
||
|
|
- `error` (Error | null) - Connection error
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### useMenu
|
||
|
|
|
||
|
|
Hook for menu real-time updates.
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
const { menu, loading, subscribed, subscribe, unsubscribe, request } = useMenu(
|
||
|
|
socket,
|
||
|
|
userId,
|
||
|
|
{
|
||
|
|
autoSubscribe: true,
|
||
|
|
},
|
||
|
|
);
|
||
|
|
|
||
|
|
// Or manual subscription
|
||
|
|
useEffect(() => {
|
||
|
|
subscribe();
|
||
|
|
return () => unsubscribe();
|
||
|
|
}, [subscribe, unsubscribe]);
|
||
|
|
```
|
||
|
|
|
||
|
|
**Events:**
|
||
|
|
- `menu:updated` - Menu items updated
|
||
|
|
|
||
|
|
**Returns:**
|
||
|
|
- `menu` (MenuItem[] | null) - Current menu items
|
||
|
|
- `loading` (boolean) - Request loading state
|
||
|
|
- `subscribed` (boolean) - Subscription status
|
||
|
|
- `subscribe()` - Subscribe to menu updates
|
||
|
|
- `unsubscribe()` - Unsubscribe from menu updates
|
||
|
|
- `request()` - Request current menu data
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### useGoal
|
||
|
|
|
||
|
|
Hook for goal progress and completion updates.
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
const { goals, subscribed, subscribe, unsubscribe } = useGoal(
|
||
|
|
socket,
|
||
|
|
userId,
|
||
|
|
{
|
||
|
|
autoSubscribe: true,
|
||
|
|
onProgress: (goal) => console.log('Goal progress:', goal),
|
||
|
|
onCompleted: (goal) => showCelebration(goal),
|
||
|
|
},
|
||
|
|
);
|
||
|
|
```
|
||
|
|
|
||
|
|
**Events:**
|
||
|
|
- `goal:progress` - Goal progress updated
|
||
|
|
- `goal:completed` - Goal completed
|
||
|
|
|
||
|
|
**Returns:**
|
||
|
|
- `goals` (Goal[]) - Active goals
|
||
|
|
- `loading` (boolean) - Request loading state
|
||
|
|
- `subscribed` (boolean) - Subscription status
|
||
|
|
- `subscribe()` - Subscribe to goal updates
|
||
|
|
- `unsubscribe()` - Unsubscribe from goal updates
|
||
|
|
- `request()` - Request current goals
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### useTip
|
||
|
|
|
||
|
|
Hook for tip notifications.
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
const { tips, latestTip, subscribe, unsubscribe, clearTips } = useTip(
|
||
|
|
socket,
|
||
|
|
userId,
|
||
|
|
{
|
||
|
|
autoSubscribe: true,
|
||
|
|
maxTips: 50,
|
||
|
|
onTipReceived: (tip) => showNotification(tip),
|
||
|
|
},
|
||
|
|
);
|
||
|
|
```
|
||
|
|
|
||
|
|
**Events:**
|
||
|
|
- `tip:received` - New tip received
|
||
|
|
|
||
|
|
**Returns:**
|
||
|
|
- `tips` (Tip[]) - Tip history (newest first)
|
||
|
|
- `latestTip` (Tip | null) - Most recent tip
|
||
|
|
- `subscribed` (boolean) - Subscription status
|
||
|
|
- `subscribe()` - Subscribe to tip notifications
|
||
|
|
- `unsubscribe()` - Unsubscribe from tip notifications
|
||
|
|
- `clearTips()` - Clear tip history
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### useChatbot
|
||
|
|
|
||
|
|
Hook for chatbot persona-based AI interactions.
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
const { messages, sendMessage, subscribed } = useChatbot(
|
||
|
|
socket,
|
||
|
|
userId,
|
||
|
|
roomId,
|
||
|
|
{
|
||
|
|
autoSubscribe: true,
|
||
|
|
maxMessages: 100,
|
||
|
|
onResponse: (response) => console.log('Bot says:', response.message),
|
||
|
|
onError: (error) => console.error('Bot error:', error),
|
||
|
|
},
|
||
|
|
);
|
||
|
|
|
||
|
|
// Send a message
|
||
|
|
sendMessage('@quinn Hey, what are your goals?');
|
||
|
|
```
|
||
|
|
|
||
|
|
**Events:**
|
||
|
|
- `chatbot:response` - AI response received
|
||
|
|
- `chatbot:error` - Error processing message
|
||
|
|
|
||
|
|
**Persona Routing:**
|
||
|
|
- `@quinn`, `@quin` → Quinn (performer persona)
|
||
|
|
- `@quinnbot`, `@quinbot`, `@qbot` → QBot (assistant persona)
|
||
|
|
|
||
|
|
**Returns:**
|
||
|
|
- `messages` (ChatMessage[]) - Chat history
|
||
|
|
- `subscribed` (boolean) - Subscription status
|
||
|
|
- `subscribe()` - Subscribe to chatbot events
|
||
|
|
- `unsubscribe()` - Unsubscribe from chatbot events
|
||
|
|
- `sendMessage(message)` - Send a message to chatbot
|
||
|
|
- `clearMessages()` - Clear message history
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Complete Example
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
import {
|
||
|
|
useWebSocket,
|
||
|
|
useMenu,
|
||
|
|
useGoal,
|
||
|
|
useTip,
|
||
|
|
useChatbot,
|
||
|
|
} from '@lilith/websocket-client';
|
||
|
|
|
||
|
|
function PerformerDashboard({ userId, token }) {
|
||
|
|
// Connect to WebSocket
|
||
|
|
const { socket, connected } = useWebSocket({
|
||
|
|
url: 'ws://localhost:4001',
|
||
|
|
token,
|
||
|
|
});
|
||
|
|
|
||
|
|
// Subscribe to menu updates
|
||
|
|
const { menu } = useMenu(socket, userId, { autoSubscribe: true });
|
||
|
|
|
||
|
|
// Subscribe to goal updates with callbacks
|
||
|
|
const { goals } = useGoal(socket, userId, {
|
||
|
|
autoSubscribe: true,
|
||
|
|
onProgress: (goal) => console.log('Goal progress:', goal.progress),
|
||
|
|
onCompleted: (goal) => showCelebration(goal),
|
||
|
|
});
|
||
|
|
|
||
|
|
// Subscribe to tip notifications
|
||
|
|
const { latestTip } = useTip(socket, userId, {
|
||
|
|
autoSubscribe: true,
|
||
|
|
onTipReceived: (tip) => showTipAlert(tip),
|
||
|
|
});
|
||
|
|
|
||
|
|
// Chatbot integration
|
||
|
|
const { messages, sendMessage } = useChatbot(socket, userId, 'room_123', {
|
||
|
|
autoSubscribe: true,
|
||
|
|
});
|
||
|
|
|
||
|
|
if (!connected) return <div>Connecting...</div>;
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div>
|
||
|
|
<h1>Dashboard</h1>
|
||
|
|
|
||
|
|
<section>
|
||
|
|
<h2>Menu ({menu?.length || 0} items)</h2>
|
||
|
|
{menu?.map((item) => (
|
||
|
|
<div key={item.id}>{item.title}</div>
|
||
|
|
))}
|
||
|
|
</section>
|
||
|
|
|
||
|
|
<section>
|
||
|
|
<h2>Goals</h2>
|
||
|
|
{goals.map((goal) => (
|
||
|
|
<div key={goal.id}>
|
||
|
|
{goal.title}: {goal.progress}%
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
</section>
|
||
|
|
|
||
|
|
<section>
|
||
|
|
<h2>Latest Tip</h2>
|
||
|
|
{latestTip && (
|
||
|
|
<div>
|
||
|
|
{latestTip.tipperName} tipped {latestTip.amount} tokens!
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</section>
|
||
|
|
|
||
|
|
<section>
|
||
|
|
<h2>Chat with AI</h2>
|
||
|
|
<div>
|
||
|
|
{messages.map((msg) => (
|
||
|
|
<div key={msg.id}>
|
||
|
|
<strong>{msg.sender === 'user' ? 'You' : msg.personaName}:</strong>{' '}
|
||
|
|
{msg.message}
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
<input
|
||
|
|
onKeyDown={(e) => {
|
||
|
|
if (e.key === 'Enter') {
|
||
|
|
sendMessage(e.currentTarget.value);
|
||
|
|
e.currentTarget.value = '';
|
||
|
|
}
|
||
|
|
}}
|
||
|
|
placeholder="Type @quinn or @qbot..."
|
||
|
|
/>
|
||
|
|
</section>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Messaging Namespaces (Stream 18)
|
||
|
|
|
||
|
|
### ChatNamespace (/chat)
|
||
|
|
|
||
|
|
Direct messaging with 1-on-1 and group chat support.
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
import { SocketClient } from '@lilith/websocket-client'
|
||
|
|
|
||
|
|
const client = new SocketClient({
|
||
|
|
url: 'ws://localhost:4001',
|
||
|
|
auth: { userId: 'user_123' },
|
||
|
|
})
|
||
|
|
|
||
|
|
const chat = client.chat()
|
||
|
|
await chat.connect()
|
||
|
|
|
||
|
|
// Join a room
|
||
|
|
const response = await chat.joinRoom('room_abc')
|
||
|
|
|
||
|
|
// Send a message
|
||
|
|
await chat.sendMessage({
|
||
|
|
roomId: 'room_abc',
|
||
|
|
content: 'Hello!',
|
||
|
|
})
|
||
|
|
|
||
|
|
// Listen for messages
|
||
|
|
chat.onMessage((message) => {
|
||
|
|
console.log('New message:', message)
|
||
|
|
})
|
||
|
|
|
||
|
|
// Typing indicators
|
||
|
|
chat.onTyping((data) => {
|
||
|
|
console.log(`${data.userId} is typing...`)
|
||
|
|
})
|
||
|
|
|
||
|
|
chat.sendTyping('room_abc', true) // Start typing
|
||
|
|
chat.sendTyping('room_abc', false) // Stop typing
|
||
|
|
|
||
|
|
// Mark as read
|
||
|
|
await chat.markAsRead('message_id')
|
||
|
|
|
||
|
|
// Cleanup
|
||
|
|
chat.leaveRoom('room_abc')
|
||
|
|
chat.disconnect()
|
||
|
|
```
|
||
|
|
|
||
|
|
### BroadcastNamespace (/broadcast)
|
||
|
|
|
||
|
|
High-volume live chat for streams and broadcasts with SuperChat support.
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
const broadcast = client.broadcast()
|
||
|
|
await broadcast.connect()
|
||
|
|
|
||
|
|
// Join broadcast
|
||
|
|
const response = await broadcast.joinBroadcast('stream_xyz')
|
||
|
|
console.log('Viewer count:', response.viewerCount)
|
||
|
|
|
||
|
|
// Send message
|
||
|
|
await broadcast.sendMessage({
|
||
|
|
roomId: 'stream_xyz',
|
||
|
|
content: 'Great stream!',
|
||
|
|
})
|
||
|
|
|
||
|
|
// Send SuperChat
|
||
|
|
await broadcast.sendMessage({
|
||
|
|
roomId: 'stream_xyz',
|
||
|
|
content: 'Amazing content!',
|
||
|
|
superChatAmount: 100,
|
||
|
|
superChatCurrency: 'USD',
|
||
|
|
})
|
||
|
|
|
||
|
|
// Listen for messages
|
||
|
|
broadcast.onMessage((message) => {
|
||
|
|
console.log('Chat:', message)
|
||
|
|
})
|
||
|
|
|
||
|
|
// Listen for SuperChats
|
||
|
|
broadcast.onSuperChat((data) => {
|
||
|
|
console.log(`SuperChat: $${data.amount} from ${data.message.senderId}`)
|
||
|
|
})
|
||
|
|
|
||
|
|
// Listen for viewer count
|
||
|
|
broadcast.onViewerCount((data) => {
|
||
|
|
console.log('Viewers:', data.count)
|
||
|
|
})
|
||
|
|
|
||
|
|
// Send emoji reaction
|
||
|
|
await broadcast.sendEmoji('stream_xyz', '❤️')
|
||
|
|
|
||
|
|
// React to message
|
||
|
|
broadcast.sendReaction('stream_xyz', 'message_id', '👍')
|
||
|
|
|
||
|
|
// Vote in poll
|
||
|
|
broadcast.sendPollVote('stream_xyz', 'poll_id', 'option_a')
|
||
|
|
|
||
|
|
// Cleanup
|
||
|
|
broadcast.leaveBroadcast('stream_xyz')
|
||
|
|
broadcast.disconnect()
|
||
|
|
```
|
||
|
|
|
||
|
|
### SocketClient API
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
const client = new SocketClient(config)
|
||
|
|
|
||
|
|
// Namespace accessors
|
||
|
|
const chatNamespace = client.chat()
|
||
|
|
const broadcastNamespace = client.broadcast()
|
||
|
|
|
||
|
|
// Disconnect all namespaces
|
||
|
|
client.disconnectAll()
|
||
|
|
```
|
||
|
|
|
||
|
|
## Direct Client Usage (No React)
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
import { WebSocketClient } from '@lilith/websocket-client';
|
||
|
|
|
||
|
|
const client = new WebSocketClient({
|
||
|
|
url: 'ws://localhost:4001',
|
||
|
|
token: 'your-jwt-token',
|
||
|
|
});
|
||
|
|
|
||
|
|
const socket = client.getSocket();
|
||
|
|
|
||
|
|
// Subscribe to menu updates
|
||
|
|
socket?.emit('menu:subscribe', { userId: 'user_123' });
|
||
|
|
|
||
|
|
socket?.on('menu:updated', (data) => {
|
||
|
|
console.log('Menu updated:', data.menu);
|
||
|
|
});
|
||
|
|
|
||
|
|
// Cleanup
|
||
|
|
client.disconnect();
|
||
|
|
```
|
||
|
|
|
||
|
|
## Type Definitions
|
||
|
|
|
||
|
|
All events and payloads are fully typed. Import types as needed:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
import type {
|
||
|
|
MenuItem,
|
||
|
|
Goal,
|
||
|
|
Tip,
|
||
|
|
ChatbotResponsePayload,
|
||
|
|
MenuUpdatedPayload,
|
||
|
|
GoalProgressPayload,
|
||
|
|
} from '@lilith/websocket-client';
|
||
|
|
```
|
||
|
|
|
||
|
|
## Development
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Type check
|
||
|
|
pnpm typecheck
|
||
|
|
|
||
|
|
# Build
|
||
|
|
pnpm build
|
||
|
|
|
||
|
|
# Test
|
||
|
|
pnpm test
|
||
|
|
|
||
|
|
# Lint
|
||
|
|
pnpm lint
|
||
|
|
```
|
||
|
|
|
||
|
|
## Architecture
|
||
|
|
|
||
|
|
This library wraps Socket.IO client and provides:
|
||
|
|
|
||
|
|
1. **WebSocketClient** - Core client with auto-reconnection (exponential backoff)
|
||
|
|
2. **React Hooks** - State management and event handling
|
||
|
|
3. **Type Safety** - Full TypeScript definitions for all events
|
||
|
|
4. **Developer Experience** - Simple API, sensible defaults, cleanup handling
|
||
|
|
|
||
|
|
## Troubleshooting
|
||
|
|
|
||
|
|
### Connection Issues
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
const { error } = useWebSocket({ url: 'ws://localhost:4001', token });
|
||
|
|
|
||
|
|
if (error) {
|
||
|
|
console.error('Connection error:', error.message);
|
||
|
|
// Common issues:
|
||
|
|
// - WebSocket service not running
|
||
|
|
// - Invalid JWT token
|
||
|
|
// - CORS configuration
|
||
|
|
// - Firewall blocking port 4001
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Subscriptions Not Working
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
// Make sure socket is connected before subscribing
|
||
|
|
const { socket, connected } = useWebSocket({ ... });
|
||
|
|
const { subscribe } = useMenu(socket, userId);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
if (connected) {
|
||
|
|
subscribe();
|
||
|
|
}
|
||
|
|
}, [connected, subscribe]);
|
||
|
|
```
|
||
|
|
|
||
|
|
### Missing Events
|
||
|
|
|
||
|
|
Check that you're subscribed to the correct userId/roomId and that the WebSocket service is emitting to the correct rooms.
|
||
|
|
|
||
|
|
## License
|
||
|
|
|
||
|
|
Private - Part of lilith-platform monorepo
|