Leaderboard
Sorted leaderboard with ranked member/score entries, top-N queries, and rank lookups backed by Grove KV.
The Leaderboard extension provides a sorted leaderboard that stores member/score pairs in a KV-backed map. Scores are sorted on read, giving you top-N queries, rank lookups, and member management through a simple API.
Installation
import "github.com/xraph/grove/kv/plugins"Creating a Leaderboard
board := plugins.NewLeaderboard(store, "scores")Parameters:
| Parameter | Type | Description |
|---|---|---|
store | *kv.Store | The backing KV store |
key | string | Logical name for the leaderboard (auto-prefixed with lb:) |
The entire leaderboard is stored as a single serialized map[string]float64 at lb:<key>.
API Reference
Add
Adds a member with a score, or updates the score if the member already exists.
func (lb *Leaderboard) Add(ctx context.Context, member string, score float64) errorerr := board.Add(ctx, "alice", 2500)
err = board.Add(ctx, "bob", 3100)
err = board.Add(ctx, "carol", 1800)
// Update alice's score.
err = board.Add(ctx, "alice", 2750)Score
Returns the score for a specific member. Returns kv.ErrNotFound if the member is not on the leaderboard.
func (lb *Leaderboard) Score(ctx context.Context, member string) (float64, error)score, err := board.Score(ctx, "alice")
// score = 2750.0TopN
Returns the top N entries sorted by score in descending order (highest first).
func (lb *Leaderboard) TopN(ctx context.Context, n int) ([]LeaderboardEntry, error)top, err := board.TopN(ctx, 10)
for _, entry := range top {
fmt.Printf("#%d %s: %.0f\n", i+1, entry.Member, entry.Score)
}
// #1 bob: 3100
// #2 alice: 2750
// #3 carol: 1800If n is greater than the number of members, all members are returned.
Rank
Returns the 1-based rank of a member (1 = highest score). Returns kv.ErrNotFound if the member is not on the leaderboard.
func (lb *Leaderboard) Rank(ctx context.Context, member string) (int, error)rank, err := board.Rank(ctx, "alice")
// rank = 2 (bob has a higher score)Remove
Removes a member from the leaderboard.
func (lb *Leaderboard) Remove(ctx context.Context, member string) errorerr := board.Remove(ctx, "carol")Size
Returns the number of members on the leaderboard.
func (lb *Leaderboard) Size(ctx context.Context) (int, error)n, err := board.Size(ctx)
// n = 2LeaderboardEntry
The TopN method returns a slice of LeaderboardEntry values:
type LeaderboardEntry struct {
Member string `json:"member"`
Score float64 `json:"score"`
}| Field | Type | Description |
|---|---|---|
Member | string | The member identifier |
Score | float64 | The member's score |
Example: Game Leaderboard
package main
import (
"context"
"encoding/json"
"fmt"
"net/http"
"github.com/xraph/grove/kv"
"github.com/xraph/grove/kv/plugins"
"github.com/xraph/grove/kv/drivers/redisdriver"
)
func main() {
ctx := context.Background()
rdb := redisdriver.New()
rdb.Open(ctx, "redis://localhost:6379/0")
store, _ := kv.Open(rdb)
defer store.Close()
board := plugins.NewLeaderboard(store, "game-highscores")
mux := http.NewServeMux()
// Submit a score.
mux.HandleFunc("POST /score", func(w http.ResponseWriter, r *http.Request) {
var req struct {
Player string `json:"player"`
Score float64 `json:"score"`
}
json.NewDecoder(r.Body).Decode(&req)
if err := board.Add(r.Context(), req.Player, req.Score); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
rank, _ := board.Rank(r.Context(), req.Player)
json.NewEncoder(w).Encode(map[string]any{
"player": req.Player,
"score": req.Score,
"rank": rank,
})
})
// Get the top 10.
mux.HandleFunc("GET /leaderboard", func(w http.ResponseWriter, r *http.Request) {
top, err := board.TopN(r.Context(), 10)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
type entry struct {
Rank int `json:"rank"`
Player string `json:"player"`
Score float64 `json:"score"`
}
results := make([]entry, len(top))
for i, e := range top {
results[i] = entry{Rank: i + 1, Player: e.Member, Score: e.Score}
}
json.NewEncoder(w).Encode(results)
})
fmt.Println("listening on :8080")
http.ListenAndServe(":8080", mux)
}Key Layout
The leaderboard is stored as a single key:
lb:<key>For example:
lb:game-highscores
lb:scoresThe value is a serialized map[string]float64 containing all member/score pairs.
Implementation Notes
- Sorting happens on read. Each call to
TopNorRankloads the full score map and sorts it. This is efficient for leaderboards with up to tens of thousands of members. - For very large leaderboards (100K+ members) backed by Redis, consider using Redis sorted sets directly via
store.Unwrap()for O(log N) rank lookups. - Scores are
float64, supporting both integer and fractional scoring systems. - Rank ties are not explicitly handled -- members with the same score may appear in any order relative to each other, but they will share the same rank value from the
Rankmethod.