11import { v } from 'convex/values'
22import { internal } from './_generated/api'
3- import type { Id } from './_generated/dataModel'
43import { internalAction , internalMutation , internalQuery } from './functions'
54import {
6- buildTrendingLeaderboard ,
7- compareTrendingEntries ,
5+ buildTrendingEntriesFromDailyRows ,
6+ buildTrendingEntryCandidates ,
87 getTrendingRange ,
98 queryDailyStats ,
10- topN ,
9+ takeTopNonSuspiciousTrendingEntries ,
10+ takeTopTrendingEntries ,
11+ TRENDING_LEADERBOARD_KIND ,
12+ TRENDING_NON_SUSPICIOUS_LEADERBOARD_KIND ,
1113} from './lib/leaderboards'
1214
1315const MAX_TRENDING_LIMIT = 200
@@ -26,9 +28,27 @@ export const getDailyStats = internalQuery({
2628 } ,
2729} )
2830
31+ export const filterTopNonSuspiciousTrendingEntries = internalQuery ( {
32+ args : {
33+ entries : v . array (
34+ v . object ( {
35+ skillId : v . id ( 'skills' ) ,
36+ score : v . number ( ) ,
37+ installs : v . number ( ) ,
38+ downloads : v . number ( ) ,
39+ } ) ,
40+ ) ,
41+ limit : v . number ( ) ,
42+ } ,
43+ handler : async ( ctx , { entries, limit } ) => {
44+ return takeTopNonSuspiciousTrendingEntries ( ctx , entries , limit )
45+ } ,
46+ } )
47+
2948/** Writes the pre-computed leaderboard and prunes old entries. */
3049export const writeTrendingLeaderboard = internalMutation ( {
3150 args : {
51+ kind : v . string ( ) ,
3252 items : v . array (
3353 v . object ( {
3454 skillId : v . id ( 'skills' ) ,
@@ -40,11 +60,11 @@ export const writeTrendingLeaderboard = internalMutation({
4060 startDay : v . number ( ) ,
4161 endDay : v . number ( ) ,
4262 } ,
43- handler : async ( ctx , { items, startDay, endDay } ) => {
63+ handler : async ( ctx , { kind , items, startDay, endDay } ) => {
4464 const now = Date . now ( )
4565
4666 await ctx . db . insert ( 'skillLeaderboards' , {
47- kind : 'trending' ,
67+ kind,
4868 generatedAt : now ,
4969 rangeStartDay : startDay ,
5070 rangeEndDay : endDay ,
@@ -53,7 +73,7 @@ export const writeTrendingLeaderboard = internalMutation({
5373
5474 const recent = await ctx . db
5575 . query ( 'skillLeaderboards' )
56- . withIndex ( 'by_kind' , ( q ) => q . eq ( 'kind' , 'trending' ) )
76+ . withIndex ( 'by_kind' , ( q ) => q . eq ( 'kind' , kind ) )
5777 . order ( 'desc' )
5878 . take ( KEEP_LEADERBOARD_ENTRIES + 5 )
5979
@@ -70,39 +90,32 @@ export const rebuildTrendingLeaderboardAction = internalAction({
7090 args : { limit : v . optional ( v . number ( ) ) } ,
7191 handler : async ( ctx , args ) : Promise < { ok : true ; count : number } > => {
7292 const limit = clampInt ( args . limit ?? MAX_TRENDING_LIMIT , 1 , MAX_TRENDING_LIMIT )
73- const { startDay , endDay } = getTrendingRange ( Date . now ( ) )
74-
93+ const now = Date . now ( )
94+ const { startDay , endDay } = getTrendingRange ( now )
7595 const dayKeys = Array . from ( { length : endDay - startDay + 1 } , ( _ , i ) => startDay + i )
7696 const perDayRows = await Promise . all (
7797 dayKeys . map ( ( day ) => ctx . runQuery ( internal . leaderboards . getDailyStats , { day } ) ) ,
7898 )
79-
80- const totals = new Map < string , { installs : number ; downloads : number } > ( )
81- for ( const rows of perDayRows ) {
82- for ( const row of rows ) {
83- const current = totals . get ( row . skillId ) ?? { installs : 0 , downloads : 0 }
84- current . installs += row . installs
85- current . downloads += row . downloads
86- totals . set ( row . skillId , current )
87- }
88- }
89-
90- const entries = Array . from ( totals , ( [ skillId , t ] ) => ( {
91- skillId : skillId as Id < 'skills' > ,
92- installs : t . installs ,
93- downloads : t . downloads ,
94- score : t . installs ,
95- } ) )
96-
97- const items = topN ( entries , limit , compareTrendingEntries ) . sort ( ( a , b ) =>
98- compareTrendingEntries ( b , a ) ,
99+ const entries = buildTrendingEntriesFromDailyRows ( perDayRows )
100+ const items = takeTopTrendingEntries ( entries , limit )
101+ const nonSuspicious = await ctx . runQuery (
102+ internal . leaderboards . filterTopNonSuspiciousTrendingEntries ,
103+ { entries, limit } ,
99104 )
100105
101- return await ctx . runMutation ( internal . leaderboards . writeTrendingLeaderboard , {
106+ await ctx . runMutation ( internal . leaderboards . writeTrendingLeaderboard , {
107+ kind : TRENDING_LEADERBOARD_KIND ,
102108 items,
103109 startDay,
104110 endDay,
105111 } )
112+ await ctx . runMutation ( internal . leaderboards . writeTrendingLeaderboard , {
113+ kind : TRENDING_NON_SUSPICIOUS_LEADERBOARD_KIND ,
114+ items : nonSuspicious ,
115+ startDay,
116+ endDay,
117+ } )
118+ return { ok : true as const , count : items . length }
106119 } ,
107120} )
108121
@@ -115,24 +128,37 @@ export const rebuildTrendingLeaderboardInternal = internalMutation({
115128 handler : async ( ctx , args ) => {
116129 const limit = clampInt ( args . limit ?? MAX_TRENDING_LIMIT , 1 , MAX_TRENDING_LIMIT )
117130 const now = Date . now ( )
118- const { startDay, endDay, items } = await buildTrendingLeaderboard ( ctx , { limit, now } )
131+ const { startDay, endDay, entries } = await buildTrendingEntryCandidates ( ctx , now )
132+ const items = takeTopTrendingEntries ( entries , limit )
133+ const nonSuspicious = await takeTopNonSuspiciousTrendingEntries ( ctx , entries , limit )
119134
120135 await ctx . db . insert ( 'skillLeaderboards' , {
121- kind : 'trending' ,
136+ kind : TRENDING_LEADERBOARD_KIND ,
122137 generatedAt : now ,
123138 rangeStartDay : startDay ,
124139 rangeEndDay : endDay ,
125140 items,
126141 } )
142+ await ctx . db . insert ( 'skillLeaderboards' , {
143+ kind : TRENDING_NON_SUSPICIOUS_LEADERBOARD_KIND ,
144+ generatedAt : now ,
145+ rangeStartDay : startDay ,
146+ rangeEndDay : endDay ,
147+ items : nonSuspicious ,
148+ } )
127149
128- const recent = await ctx . db
129- . query ( 'skillLeaderboards' )
130- . withIndex ( 'by_kind' , ( q ) => q . eq ( 'kind' , 'trending' ) )
131- . order ( 'desc' )
132- . take ( KEEP_LEADERBOARD_ENTRIES + 5 )
133-
134- for ( const entry of recent . slice ( KEEP_LEADERBOARD_ENTRIES ) ) {
135- await ctx . db . delete ( entry . _id )
150+ for ( const kind of [
151+ TRENDING_LEADERBOARD_KIND ,
152+ TRENDING_NON_SUSPICIOUS_LEADERBOARD_KIND ,
153+ ] ) {
154+ const entriesForKind = await ctx . db
155+ . query ( 'skillLeaderboards' )
156+ . withIndex ( 'by_kind' , ( q ) => q . eq ( 'kind' , kind ) )
157+ . order ( 'desc' )
158+ . take ( KEEP_LEADERBOARD_ENTRIES + 5 )
159+ for ( const entry of entriesForKind . slice ( KEEP_LEADERBOARD_ENTRIES ) ) {
160+ await ctx . db . delete ( entry . _id )
161+ }
136162 }
137163
138164 return { ok : true as const , count : items . length }
0 commit comments