Skip to content

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/


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>
)
}
  • 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