Bỏ qua

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

  1. Có cần real-time cursor tracking không?
  2. Typing indicator trong chat?
  3. Collaborative features nào khác?