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.
Installation01
import "github.com/xraph/grove/kv/plugins"Creating a Leaderboard02
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 Reference03
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 = 2LeaderboardEntry04
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 Leaderboard05
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 Layout06
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 Notes07
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.