Broadcast Messages
Broadcast là gì?
Client-to-Client Messaging
┌─────────────────────────────────────────────────────────────┐
│ BROADCAST │
├─────────────────────────────────────────────────────────────┤
│ │
│ Client A ──────────┐ │
│ (sends) │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ Channel │ ─────▶ Client B (receives) │
│ │ "room-1" │ │
│ └─────────────┘ ─────▶ Client C (receives) │
│ │ │
│ └──────▶ Client A (receives own msg*) │
│ │
│ * Optional: self: true để nhận lại message của mình │
│ │
│ Use cases: │
│ - Cursor tracking │
│ - Typing indicators │
│ - Game state sync │
│ - Ephemeral data (không cần lưu DB) │
│ │
└─────────────────────────────────────────────────────────────┘
Basic Broadcast
Send and Receive
// Create channel
const channel = supabase.channel('room-1');
// Listen for broadcast events
channel.on(
'broadcast',
{ event: 'cursor-move' },
(payload) => {
console.log('Cursor position:', payload.payload);
}
);
// Subscribe first
await channel.subscribe();
// Send broadcast message
channel.send({
type: 'broadcast',
event: 'cursor-move',
payload: { x: 100, y: 200, userId: 'user-123' },
});
Receive Own Messages
// By default, sender doesn't receive own message
// Enable with self: true
const channel = supabase.channel('room-1', {
config: {
broadcast: { self: true }, // Receive own broadcasts
},
});
Cursor Tracking Example
React Component
'use client';
import { useEffect, useState, useRef } from 'react';
import { createClient } from '@/lib/supabase/client';
type CursorPosition = {
x: number;
y: number;
userId: string;
color: string;
};
export function CollaborativeCursors({ roomId }: { roomId: string }) {
const [cursors, setCursors] = useState<Record<string, CursorPosition>>({});
const channelRef = useRef<any>(null);
const supabase = createClient();
const userId = useRef(crypto.randomUUID());
const color = useRef(`hsl(${Math.random() * 360}, 70%, 50%)`);
useEffect(() => {
const channel = supabase.channel(`cursors:${roomId}`);
channel.on(
'broadcast',
{ event: 'cursor-move' },
({ payload }) => {
setCursors((prev) => ({
...prev,
[payload.userId]: payload,
}));
}
);
channel.on(
'broadcast',
{ event: 'cursor-leave' },
({ payload }) => {
setCursors((prev) => {
const { [payload.userId]: _, ...rest } = prev;
return rest;
});
}
);
channel.subscribe();
channelRef.current = channel;
return () => {
// Notify others when leaving
channel.send({
type: 'broadcast',
event: 'cursor-leave',
payload: { userId: userId.current },
});
supabase.removeChannel(channel);
};
}, [roomId]);
const handleMouseMove = (e: React.MouseEvent) => {
channelRef.current?.send({
type: 'broadcast',
event: 'cursor-move',
payload: {
x: e.clientX,
y: e.clientY,
userId: userId.current,
color: color.current,
},
});
};
return (
<div
onMouseMove={handleMouseMove}
style={{ position: 'relative', height: '100vh' }}
>
{Object.values(cursors).map((cursor) => (
<div
key={cursor.userId}
style={{
position: 'absolute',
left: cursor.x,
top: cursor.y,
width: 20,
height: 20,
borderRadius: '50%',
backgroundColor: cursor.color,
transform: 'translate(-50%, -50%)',
pointerEvents: 'none',
}}
/>
))}
</div>
);
}
Typing Indicator Example
Chat Typing Status
'use client';
import { useEffect, useState, useRef } from 'react';
import { createClient } from '@/lib/supabase/client';
export function useTypingIndicator(roomId: string, userId: string) {
const [typingUsers, setTypingUsers] = useState<string[]>([]);
const channelRef = useRef<any>(null);
const supabase = createClient();
useEffect(() => {
const channel = supabase.channel(`typing:${roomId}`);
channel.on(
'broadcast',
{ event: 'typing-start' },
({ payload }) => {
if (payload.userId !== userId) {
setTypingUsers((prev) =>
prev.includes(payload.userId)
? prev
: [...prev, payload.userId]
);
}
}
);
channel.on(
'broadcast',
{ event: 'typing-stop' },
({ payload }) => {
setTypingUsers((prev) =>
prev.filter((id) => id !== payload.userId)
);
}
);
channel.subscribe();
channelRef.current = channel;
return () => {
supabase.removeChannel(channel);
};
}, [roomId, userId]);
// Debounced typing notification
const typingTimeoutRef = useRef<NodeJS.Timeout>();
const notifyTyping = () => {
channelRef.current?.send({
type: 'broadcast',
event: 'typing-start',
payload: { userId },
});
// Clear previous timeout
if (typingTimeoutRef.current) {
clearTimeout(typingTimeoutRef.current);
}
// Stop typing after 2 seconds of inactivity
typingTimeoutRef.current = setTimeout(() => {
channelRef.current?.send({
type: 'broadcast',
event: 'typing-stop',
payload: { userId },
});
}, 2000);
};
return { typingUsers, notifyTyping };
}
// Usage
function ChatInput({ roomId, userId }: Props) {
const { typingUsers, notifyTyping } = useTypingIndicator(roomId, userId);
return (
<div>
<input
type="text"
onChange={notifyTyping}
placeholder="Type a message..."
/>
{typingUsers.length > 0 && (
<p>{typingUsers.join(', ')} is typing...</p>
)}
</div>
);
}
Acknowledgment Pattern
Confirm Receipt
const channel = supabase.channel('room-1', {
config: {
broadcast: { ack: true }, // Wait for server acknowledgment
},
});
await channel.subscribe();
// Send with acknowledgment
const result = await channel.send({
type: 'broadcast',
event: 'message',
payload: { text: 'Hello!' },
});
if (result === 'ok') {
console.log('Message delivered to server');
} else {
console.log('Failed to deliver:', result);
}
Broadcast vs Postgres Changes
When to Use What?
| Feature |
Broadcast |
Postgres Changes |
| Data persistence |
No |
Yes (saved to DB) |
| RLS support |
No |
Yes |
| Latency |
Lower |
Higher |
| Use case |
Ephemeral state |
Persistent data |
Examples
// Broadcast: Cursor position (ephemeral)
channel.send({
type: 'broadcast',
event: 'cursor',
payload: { x, y },
});
// Postgres Changes: Chat message (persistent)
await supabase.from('messages').insert({
room_id: roomId,
content: message,
user_id: userId,
});
// Other clients receive via postgres_changes subscription
Multiple Event Types
Organize Events
const channel = supabase.channel(`room:${roomId}`);
// Cursor events
channel.on('broadcast', { event: 'cursor:move' }, handleCursorMove);
channel.on('broadcast', { event: 'cursor:leave' }, handleCursorLeave);
// Selection events
channel.on('broadcast', { event: 'selection:start' }, handleSelectionStart);
channel.on('broadcast', { event: 'selection:change' }, handleSelectionChange);
channel.on('broadcast', { event: 'selection:end' }, handleSelectionEnd);
// Typing events
channel.on('broadcast', { event: 'typing:start' }, handleTypingStart);
channel.on('broadcast', { event: 'typing:stop' }, handleTypingStop);
await channel.subscribe();
Tổng kết
Broadcast Pattern
// 1. Create channel
const channel = supabase.channel('room-id');
// 2. Listen for events
channel.on('broadcast', { event: 'event-name' }, handler);
// 3. Subscribe
await channel.subscribe();
// 4. Send messages
channel.send({
type: 'broadcast',
event: 'event-name',
payload: { ... },
});
Best Practices
✅ Use descriptive event names
✅ Include sender ID in payload
✅ Debounce frequent events (cursor, typing)
✅ Clean up on unmount
✅ Handle leave/disconnect events
Q&A
- Có cần real-time cursor tracking không?
- Typing indicator trong chat?
- Collaborative features nào khác?