# Live Presence

> Track who is viewing a page in real time. Aerostack Presence broadcasts join/leave events and cursor positions to all channel members.

Track who is online in real time. Users announce their presence when they join, update it as they interact, and Aerostack automatically cleans up when they disconnect.

**Demonstrates:** `channel.track()`, `presence:join`, `presence:update`, `presence:leave`, auto-cleanup on disconnect

**Source:** `examples/socket-hub/src/examples/LivePresence/`

No server code required for this example — presence is entirely handled by the Aerostack WebSocket layer.

---

## Client code

```tsx

interface PeerState {
  userId: string
  name: string
  color: string
  status: 'online' | 'away'
  joinedAt: number
}

interface PresenceEvent {
  type: 'join' | 'update' | 'leave'
  userId: string
  name: string
  timestamp: number
}

const COLORS = ['#FF5733', '#3498DB', '#2ECC71', '#F39C12', '#9B59B6', '#E74C3C', '#1ABC9C']
const NAMES = ['Alice', 'Bob', 'Charlie', 'Diana', 'Eve', 'Frank', 'Grace']

function randomFrom(arr: T[]): T {
  return arr[Math.floor(Math.random() * arr.length)]
}

  const { realtime } = useAerostack()
  const [peers, setPeers] = useState([])
  const [events, setEvents] = useState([])

  const myId = useRef(`user-${Math.random().toString(36).slice(2, 8)}`)
  const myName = useRef(randomFrom(NAMES))
  const myColor = useRef(randomFrom(COLORS))

  const addEvent = (type: PresenceEvent['type'], userId: string, name: string) => {
    setEvents(prev => [
      { type, userId, name, timestamp: Date.now() },
      ...prev.slice(0, 19),  // keep last 20 events
    ])
  }

  useEffect(() => {
    const channel = realtime.channel('presence/demo-room')

    channel
      .on('presence:join', ({ data }) => {
        setPeers(prev => {
          if (prev.find(p => p.userId === data.userId)) return prev
          return [...prev, data]
        })
        addEvent('join', data.userId, data.name)
      })
      .on('presence:update', ({ data }) => {
        setPeers(prev => prev.map(p =>
          p.userId === data.userId ? { ...p, ...data } : p
        ))
      })
      .on('presence:leave', ({ data }) => {
        setPeers(prev => prev.filter(p => p.userId !== data.userId))
        addEvent('leave', data.userId, data.name)
      })
      .subscribe()

    // Announce our presence
    channel.track({
      userId: myId.current,
      name: myName.current,
      color: myColor.current,
      status: 'online',
      joinedAt: Date.now(),
    })

    // Go "away" after 30s of inactivity
    let awayTimer: ReturnType<typeof setTimeout>
    const resetAwayTimer = () => {
      clearTimeout(awayTimer)
      channel.track({
        userId: myId.current,
        name: myName.current,
        color: myColor.current,
        status: 'online',
        joinedAt: Date.now(),
      })
      awayTimer = setTimeout(() => {
        channel.track({
          userId: myId.current,
          name: myName.current,
          color: myColor.current,
          status: 'away',
          joinedAt: Date.now(),
        })
      }, 30_000)
    }

    document.addEventListener('mousemove', resetAwayTimer)
    document.addEventListener('keydown', resetAwayTimer)

    return () => {
      clearTimeout(awayTimer)
      document.removeEventListener('mousemove', resetAwayTimer)
      document.removeEventListener('keydown', resetAwayTimer)
      channel.untrack()
      channel.unsubscribe()
    }
  }, [realtime])

  return (
    <div className="max-w-2xl mx-auto p-6 space-y-8">
      <div>
        <h2 className="text-xl font-bold text-white mb-4">
          Online ({peers.length})
        </h2>
        <div className="flex gap-3 flex-wrap">
          {peers.map(peer => (
            <div
              key={peer.userId}
              className="flex items-center gap-2 px-4 py-2 rounded-full text-white text-sm font-medium"
              style={{ background: peer.color + '33', border: `1px solid ${peer.color}66` }}
            >
              
              <span style={{ color: peer.color }}>{peer.name}</span>
              {peer.userId === myId.current && (
                <span className="text-xs opacity-60">(you)</span>
              )}
            </div>
          ))}
        </div>
      </div>

      <div>
        <h2 className="text-xl font-bold text-white mb-4">Event log</h2>
        <div className="space-y-1">
          {events.map((event, i) => (
            <div key={i} className="text-sm text-gray-400">
              <span className="text-gray-600 mr-2">
                {new Date(event.timestamp).toLocaleTimeString()}
              </span>
              <span className={
                event.type === 'join' ? 'text-green-400' :
                event.type === 'leave' ? 'text-red-400' :
                'text-yellow-400'
              }>
                {event.name} {event.type === 'join' ? 'joined' : event.type === 'leave' ? 'left' : 'updated'}
              </span>
            </div>
          ))}
        </div>
      </div>
    </div>
  )
}
```

## How auto-cleanup works

- User's browser connects via WebSocket and calls `channel.track(state)`
- Aerostack stores the presence state in memory
- When the connection closes (tab closes, network loss, etc.), Aerostack starts a 90-second grace period
- If the user reconnects within 90s, their presence is restored
- After 90s, Aerostack emits `presence:leave` to all subscribers and removes the state
