LogoRobo.js

Presence

Track connected participants with heartbeat detection and stale status.

Track who's connected with custom presence data. useSyncPresence manages heartbeats, stale detection, and participant lists — ideal for lobbies, typing indicators, and user status displays.

Signature

function useSyncPresence<T = unknown, ClientData = unknown>(
  key: (string | null)[],
  options?: PresenceOptions<T>
): PresenceResult<T, ClientData>
function useSyncPresence(
  key: (string | null)[],
  options?: PresenceOptions<T>
): PresenceResult<T, ClientData>

Options

OptionTypeDefaultDescription
initialPresenceT{}Initial presence data for this client
staleTimeoutnumber5000Milliseconds before marking a user as stale
heartbeatIntervalnumber2000Interval for sending heartbeat updates (ms)

Return value

FieldTypeDescription
participantsParticipant<T, ClientData>[]All participants (you first, then sorted by ID)
updatePresence(update: Partial<T> | ((prev: T) => T)) => voidUpdate your presence data
clientIdstringYour client ID
isHostbooleanWhether you're the host
contextSyncContext<ClientData>Full sync context

Participant type

interface Participant<T, ClientData> {
  clientId: string
  user: Client<ClientData>
  presence: T
  isStale: boolean    // No heartbeat received recently
  isYou: boolean      // Is this the current client
}

Basic usage

import { useSyncPresence } from '@robojs/sync'

function Lobby() {
  const { participants, updatePresence } = useSyncPresence(['room', roomId], {
    initialPresence: { status: 'online', activity: 'idle' }
  })

  return (
    <div>
      <h2>Players ({participants.length})</h2>
      <ul>
        {participants.map((p) => (
          <li key={p.clientId} style={{ opacity: p.isStale ? 0.5 : 1 }}>
            {p.user.data?.name ?? p.clientId.slice(0, 6)}
            {''}{p.presence.status}
            {p.isStale && ' (away)'}
            {p.isYou && ' (you)'}
          </li>
        ))}
      </ul>
      <button onClick={() => updatePresence({ status: 'ready' })}>Ready up</button>
    </div>
  )
}
import { useSyncPresence } from '@robojs/sync'

function Lobby() {
  const { participants, updatePresence } = useSyncPresence(['room', roomId], {
    initialPresence: { status: 'online', activity: 'idle' }
  })

  return (
    <div>
      <h2>Players ({participants.length})</h2>
      <ul>
        {participants.map((p) => (
          <li key={p.clientId} style={{ opacity: p.isStale ? 0.5 : 1 }}>
            {p.user.data?.name ?? p.clientId.slice(0, 6)}
            {''}{p.presence.status}
            {p.isStale && ' (away)'}
            {p.isYou && ' (you)'}
          </li>
        ))}
      </ul>
      <button onClick={() => updatePresence({ status: 'ready' })}>Ready up</button>
    </div>
  )
}

Updating presence

// Partial update
updatePresence({ activity: 'typing' })

// Updater function
updatePresence((prev) => ({ ...prev, score: prev.score + 1 }))
// Partial update
updatePresence({ activity: 'typing' })

// Updater function
updatePresence((prev) => ({ ...prev, score: prev.score + 1 }))

Stale detection

Presence uses a heartbeat system to detect inactive users:

  • Each client sends heartbeat updates at heartbeatInterval (default: every 2 seconds).
  • If a client's last heartbeat is older than staleTimeout (default: 5 seconds), they're marked as isStale: true.
  • Stale detection runs every second via an internal interval timer.
  • New clients without any presence data are NOT marked as stale — they only become stale after their first heartbeat ages out.

Typing indicator example

function ChatInput() {
  const { participants, updatePresence } = useSyncPresence(['chat', roomId], {
    initialPresence: { typing: false },
    staleTimeout: 3000,
    heartbeatInterval: 1000
  })

  const typingUsers = participants.filter((p) => p.presence.typing && !p.isYou && !p.isStale)

  return (
    <div>
      <input
        onFocus={() => updatePresence({ typing: true })}
        onBlur={() => updatePresence({ typing: false })}
      />
      {typingUsers.length > 0 && (
        <p>{typingUsers.map((p) => p.user.data?.name).join(', ')} typing...</p>
      )}
    </div>
  )
}
function ChatInput() {
  const { participants, updatePresence } = useSyncPresence(['chat', roomId], {
    initialPresence: { typing: false },
    staleTimeout: 3000,
    heartbeatInterval: 1000
  })

  const typingUsers = participants.filter((p) => p.presence.typing && !p.isYou && !p.isStale)

  return (
    <div>
      <input
        onFocus={() => updatePresence({ typing: true })}
        onBlur={() => updatePresence({ typing: false })}
      />
      {typingUsers.length > 0 && (
        <p>{typingUsers.map((p) => p.user.data?.name).join(', ')} typing...</p>
      )}
    </div>
  )
}

How it works

Internally, useSyncPresence uses useSyncState with the key [...key, '__presence']. The state is a map of clientId -> { presence, lastSeen }. This means:

  • Presence data is persistent (late joiners see existing participants).
  • The state grows with each client. In long-running sessions, stale entries persist until the page refreshes.

Next steps

On this page