asobi_leaderboards (asobi v0.72.6)

View Source

Board-set reads: which boards exist, and one page of a board.

asobi_leaderboard_server answers questions about one board it already holds in ETS - top N, a player's rank, the window around a player. Neither question here can be answered that way. A board only becomes a process when someone writes to it, so the process set is not the board set; and the ETS reads are deliberately capped at 100 rows so that a caller cannot walk a board, which is exactly what paging through one means.

Both reads therefore go to Postgres, where the scores are already persisted, and the process layer contributes only the live flag. That flag is the interesting half for an operator: a board is live-but-absent for its first 30 seconds (the flush interval), and present-but-not-live once nobody has written to it since the node started.

Rank is computed by the database over the same total order the ETS table is keyed on - score descending, then player_id - so a rank read here and a rank from asobi_leaderboard_server:rank/2 agree on any flushed score.

Summary

Functions

The total order a board is ranked in, shared by the rank window and the default listing order so the two cannot drift apart.

Every board with a persisted score, plus every board that is live without one yet.

One board's entries, ranked, as a query for the shared paginator.

Functions

board_order()

-spec board_order() -> [{atom(), asc | desc}].

The total order a board is ranked in, shared by the rank window and the default listing order so the two cannot drift apart.

player_id is unique within a board, so this is total, which is also what makes offset paging safe.

boards()

-spec boards() -> {ok, [map()]} | {error, term()}.

Every board with a persisted score, plus every board that is live without one yet.

Each row is #{board_id, entries, top_score, updated_at, live}. Boards known only as a running process report entries => 0 and null aggregates: they exist, they have simply not flushed.

Capped at 1000 boards, ordered by id so the cut is stable between calls.

entries_query(BoardId)

-spec entries_query(binary()) ->
                       #kura_query{from :: atom() | module() | undefined,
                                   select :: [atom() | term()] | {exprs, [term()]},
                                   wheres :: [term()],
                                   joins :: [term()],
                                   order_bys :: [term()],
                                   group_bys :: [atom()],
                                   havings :: [term()],
                                   limit :: non_neg_integer() | undefined,
                                   offset :: non_neg_integer() | undefined,
                                   distinct :: boolean() | [atom()],
                                   lock :: binary() | undefined,
                                   prefix :: binary() | undefined,
                                   preloads :: [atom() | {atom(), list()}],
                                   ctes :: [{binary(), #kura_query{}}],
                                   combinations ::
                                       [{union | union_all | intersect | except, #kura_query{}}],
                                   include_deleted :: boolean()}.

One board's entries, ranked, as a query for the shared paginator.

Returns a query rather than rows because the count and the page have to run as one unit against the same filter - asobi_ops_page owns that - and because the caller supplies the ordering. Rank is not the caller's to order by: it is a window over board_order/0 computed before LIMIT applies, so row 501 reports rank 501 however the page itself is sorted. No partition is needed, the WHERE already narrows the window to one board.