LogoRobo.js
Packages@robojs/xp

leaderboard

Variable: leaderboard

const leaderboard: {
  get: (guildId, offset, limit, options?) => Promise<{
     entries: LeaderboardEntry[];
     total: number;
    }>;
  getRank: (guildId, userId, options?) => Promise<
     | {
     rank: number;
     total: number;
    }
     | null>;
  invalidateCache: (guildId, options?) => void;
};

Leaderboard API for ranking and leaderboard operations

High-performance leaderboard system with intelligent caching for fast queries on large servers. Designed to handle 10k+ users with under 200ms response times.

Features:

  • In-memory caching of top 100 users per guild
  • Automatic cache invalidation on XP changes
  • Paginated leaderboard retrieval
  • Efficient rank position lookup
  • Stable sort (XP desc, userId asc)
  • Cache warming on first query

Caching Behavior:

  • Cache TTL: 60 seconds (auto-refresh on expiry)
  • Cache size: Top 100 users per guild (ranked 1-100)
  • Invalidation: Automatic on xpChange, levelUp, levelDown events
  • Cold start: First query builds cache (O(n log n))
  • Warm cache: Subsequent queries are O(1)

Type declaration

NameTypeDefault valueDescription
get(guildId, offset, limit, options?) => Promise<{ entries: LeaderboardEntry[]; total: number; }>getLeaderboardCoreGet paginated leaderboard entries (offset, limit)
getRank(guildId, userId, options?) => Promise< | { rank: number; total: number; } | null>getUserRankCoreGet user's rank position (1-indexed)
invalidateCache(guildId, options?) => voidinvalidateCacheCoreManually invalidate cache for a guild (usually automatic) Supports two modes: - Specific store: Pass { storeId: 'name' } to invalidate only that store - All stores: Pass { all: true } to invalidate all stores for the guild Param Guild ID Param Optional Flashcore options or { all: true } to invalidate all stores

Examples

Get Top Users (Paginated)

import { leaderboard } from '@robojs/xp'

// Get top 10 users from default store
const top10 = await leaderboard.get('guildId', 0, 10)
top10.entries.forEach(entry => {
  console.log(`#${entry.rank}: ${entry.userId} - Level ${entry.level} (${entry.xp} XP)`)
})

// Get top 10 users from custom reputation store
const repTop10 = await leaderboard.get('guildId', 0, 10, { storeId: 'reputation' })
repTop10.entries.forEach(entry => {
  console.log(`#${entry.rank}: ${entry.userId} - Rep Level ${entry.level}`)
})

Build Leaderboard Command

import { leaderboard } from '@robojs/xp'
import { CommandInteraction } from 'discord.js'

async function handleLeaderboardCommand(interaction: CommandInteraction) {
  const page = interaction.options.getInteger('page') ?? 1
  const pageSize = 10
  const offset = (page - 1) * pageSize

  // Get leaderboard entries for this page
  const entries = await leaderboard.get(interaction.guildId, offset, pageSize)

  if (entries.length === 0) {
    await interaction.reply('No users on this page!')
    return
  }

  // Build leaderboard embed
  const description = entries
    .map(entry => `#${entry.rank}: <@${entry.userId}> - Level ${entry.level} (${entry.xp} XP)`)
    .join('\n')

  await interaction.reply({
    embeds: [{
      title: `Leaderboard - Page ${page}`,
      description
    }]
  })
}

Get User's Rank Position (multi-store)

import { leaderboard } from '@robojs/xp'

// Get user's rank in default store
const defaultRank = await leaderboard.getRank('guildId', 'userId')
if (defaultRank) {
  console.log(`Default rank: #${defaultRank.rank} out of ${defaultRank.total}`)
}

// Get user's rank in custom reputation store
const repRank = await leaderboard.getRank('guildId', 'userId', { storeId: 'reputation' })
if (repRank) {
  console.log(`Reputation rank: #${repRank.rank} out of ${repRank.total}`)
}

Build Rank Command

import { leaderboard } from '@robojs/xp'
import { CommandInteraction } from 'discord.js'

async function handleRankCommand(interaction: CommandInteraction) {
  const userId = interaction.options.getUser('user')?.id ?? interaction.user.id

  const rankInfo = await leaderboard.getRank(interaction.guildId, userId)

  if (!rankInfo) {
    await interaction.reply('This user has no XP yet!')
    return
  }

  await interaction.reply({
    embeds: [{
      title: `Rank for <@${userId}>`,
      fields: [
        { name: 'Rank', value: `#${rankInfo.rank}`, inline: true },
        { name: 'Level', value: `${rankInfo.level}`, inline: true },
        { name: 'XP', value: `${rankInfo.xp}`, inline: true }
      ],
      footer: { text: `Out of ${rankInfo.total} users` }
    }]
  })
}

Manual Cache Invalidation (Advanced)

import { leaderboard } from '@robojs/xp'

// Usually automatic, but can be manually triggered
leaderboard.invalidateCache('guildId')

// Next leaderboard.get() call will rebuild cache from Flashcore
const fresh = await leaderboard.get('guildId', 0, 10)

Remarks

  • Cache is automatically invalidated on XP changes (via event listeners)
  • First query after invalidation rebuilds cache (O(n log n))
  • Subsequent queries use cached data (O(1) for top 100)
  • Leaderboard entries are sorted: XP desc, userId asc (stable sort)
  • Rank positions are 1-indexed (rank 1 = top user)
  • getRank() returns null for users with no XP record
  • Cache TTL is 60 seconds - auto-refreshes on expiry
  • Manual invalidation is rarely needed (automatic via events)

On this page