Live Presence
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/
Client code
Section titled “Client code”import { useAerostack } from '@aerostack/react'
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<T>(arr: T[]): T { return arr[Math.floor(Math.random() * arr.length)]}
export function LivePresence() { const { realtime } = useAerostack() const [peers, setPeers] = useState<PeerState[]>([]) const [events, setEvents] = useState<PresenceEvent[]>([])
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 className={`w-2 h-2 rounded-full ${peer.status === 'online' ? 'bg-green-400' : 'bg-yellow-400'}`} /> <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
Section titled “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:leaveto all subscribers and removes the state