LCOV - code coverage report
Current view: top level - lib - redis-leaderboard.js (source / functions) Hit Total Coverage
Test: lcov.info Lines: 426 434 98.2 %
Date: 2025-04-23 17:26:57 Functions: 48 48 100.0 %
Branches: 73 78 93.6 %

           Branch data     Line data    Source code
       1            [ + ]:          1 : /**
       2                 :          1 :  * @module  redis-leaderboard
       3                 :          1 :  * @desc    The leaderboard-api Redis leaderboard module.
       4                 :          1 :  * @version 1.0.0
       5                 :          1 :  * @author  Essam A. El-Sherif
       6                 :          1 :  */
       7                 :          1 : 
       8                 :          1 : /**
       9                 :          1 :  * @class
      10                 :          1 :  * @static
      11                 :          1 :  * @desc Class to manage leaderboard system using Redis.
      12                 :          1 :  */
      13                 :          1 : export class LeaderBoard{
      14                 :          1 : 
      15                 :          1 :         /**
      16                 :          1 :          * @method   constructor
      17                 :          1 :          * @instance
      18                 :          1 :          * @memberof module:redis-leaderboard.LeaderBoard
      19                 :          1 :          * @param    {object} client - Redis client object.
      20                 :          1 :          * @param    {string} keyA - Namespace for Redis activities key(s).
      21                 :          1 :          * @param    {string} keyT - Namespace for Redis timestamp key(s).
      22                 :          1 :          * @desc     Constructs and returns a LeaderBoard object.
      23                 :          1 :          */
      24            [ + ]:          1 :         constructor(client, keyT, keyA){
      25                 :          1 :                 this.client = client;
      26                 :          1 :                 this.keyT = keyT;
      27                 :          1 :                 this.keyA = keyA;
      28                 :          1 :         }
      29                 :          1 : 
      30                 :          1 :         /**
      31                 :          1 :          * @method
      32                 :          1 :          * @instance
      33                 :          1 :          * @memberof module:redis-leaderboard.LeaderBoard
      34                 :          1 :          * @param    {string} activity.
      35                 :          1 :          * @param    {string} username.
      36                 :          1 :          * @param    {number} score.
      37                 :          1 :          * @return   {Promise} A Promise object that will fulfill upon adding the user's activity/score.
      38                 :          1 :          * @desc     Add a user activity/score.
      39                 :          1 :          */
      40            [ + ]:          1 :         addUserScore(activity, username, score){
      41                 :          5 : 
      42                 :          5 :                 let timestamp = String(Date.now());
      43                 :          5 :                 score = String(score);
      44                 :          5 : 
      45                 :          5 :                 return this.client.sendCommand([
      46                 :          5 :                         'HSET',
      47                 :          5 :                         `${this.keyT}${activity}:${username}`,
      48                 :          5 :                         'timestamp', timestamp,
      49                 :          5 :                 ])
      50            [ + ]:          5 :                 .then(()=> {
      51                 :          5 :                         return this.client.sendCommand([
      52                 :          5 :                                 'ZADD',
      53                 :          5 :                                 `${this.keyA}${activity}`,
      54                 :          5 :                                 score, username
      55                 :          5 :                         ]);
      56                 :          5 :                 })
      57            [ + ]:          5 :                 .then(() => {
      58                 :          5 :                         return {activity, username, score, timestamp};
      59                 :          5 :                 });
      60                 :          5 :         }
      61                 :          1 : 
      62                 :          1 :         /**
      63                 :          1 :          * @method
      64                 :          1 :          * @instance
      65                 :          1 :          * @memberof module:redis-leaderboard.LeaderBoard
      66                 :          1 :          * @param    {string} username.
      67                 :          1 :          * @param    {string} activity - (Optional).
      68                 :          1 :          * @return   {Promise} A Promise object that will fulfill upon removing the user's activity/score.
      69                 :          1 :          * @desc     Remove a user's activity/score, or remove all user activities if second parameter was not given.
      70                 :          1 :          */
      71            [ + ]:          1 :         removeUserScore(username, activity){
      72                 :          2 : 
      73                 :          2 :                 let result;
      74                 :          2 : 
      75                 :          2 :                 return this.client.sendCommand([
      76                 :          2 :                         'KEYS',
      77            [ - ]:          2 :                         `${this.keyT}${activity || '*'}:${username}`,
      78                 :          2 :                 ])
      79            [ + ]:          2 :                 .then((res) => {
      80            [ + ]:          2 :                         res = res.filter(str => str.endsWith(username));
      81            [ + ]:          2 :                         result = res.map(key => ({
      82                 :          1 :                                                 activity: key.replace(`${this.keyT}`, '').replace(`:${username}`, ''),
      83                 :          1 :                                                 username
      84                 :          2 :                                         }));
      85                 :          2 :                         if(res.length)
      86       [ + ][ + ]:          2 :                                 return Promise.all(res.map(tsKey => this.client.sendCommand(['DEL', tsKey])));
      87                 :          1 :                         else return null;
      88                 :          2 :                 })
      89            [ + ]:          2 :                 .then((res) => {
      90                 :          2 :                         if(res)
      91  [ + ][ - ][ + ]:          2 :                                 return this.client.sendCommand(['KEYS', `${this.keyA}${activity || '*'}`]);
      92                 :          1 :                         else return null;
      93                 :          2 :                 })
      94            [ + ]:          2 :                 .then((res) => {
      95                 :          2 :                         if(res)
      96       [ + ][ + ]:          2 :                                 return Promise.all(res.map(key => this.client.sendCommand(['ZREM', key, username])));
      97                 :          1 :                         else return null;
      98                 :          2 :                 })
      99            [ + ]:          2 :                 .then(() => {
     100                 :          2 :                         return result;
     101                 :          2 :                 });
     102                 :          2 :         }
     103                 :          1 : 
     104                 :          1 :         /**
     105                 :          1 :          * @method
     106                 :          1 :          * @instance
     107                 :          1 :          * @memberof module:redis-leaderboard.LeaderBoard
     108                 :          1 :          * @param    {string} activity.
     109                 :          1 :          * @param    {string} username.
     110                 :          1 :          * @return   {Promise} A Promise object that will fulfill with the user score and rank object.
     111                 :          1 :          * @desc     Return a user score and rank object.
     112                 :          1 :          * @throws   {Error} Throws an error if the given activity does not exist.
     113                 :          1 :          */
     114            [ + ]:          1 :         getUserScoreAndRank(activity, username){
     115                 :          4 :                 let score, rank, timestamp;
     116                 :          4 : 
     117                 :          4 :                 return this.client.sendCommand([
     118                 :          4 :                         'KEYS',
     119                 :          4 :                         `${this.keyA}${activity}`,
     120                 :          4 :                 ])
     121            [ + ]:          4 :                 .then((res) => {
     122            [ + ]:          4 :                         if(!res.length){
     123                 :          1 :                                 throw new Error(`no activity '${activity}'`);
     124            [ + ]:          1 :                         }
     125                 :          3 :                         return this.client.sendCommand([
     126                 :          3 :                                 'ZSCORE',
     127                 :          3 :                                 `${this.keyA}${activity}`, username
     128                 :          3 :                         ]);
     129                 :          4 :                 })
     130            [ + ]:          4 :                 .then((res) => {
     131            [ + ]:          3 :                         if(res !== null){
     132                 :          2 :                                 score = res;
     133                 :          2 :                                 return this.client.sendCommand([
     134                 :          2 :                                         'ZREVRANK',
     135                 :          2 :                                         `${this.keyA}${activity}`, username
     136                 :          2 :                                 ]);
     137            [ + ]:          2 :                         }
     138                 :          1 :                         else return res;
     139                 :          4 :                 })
     140            [ + ]:          4 :                 .then((res) => {
     141            [ + ]:          3 :                         if(res !== null){
     142                 :          2 :                                 rank = res + 1;
     143                 :          2 :                                 return this.client.sendCommand([
     144                 :          2 :                                         'HGET',
     145                 :          2 :                                         `${this.keyT}${activity}:${username}`, 'timestamp'
     146                 :          2 :                                 ]);
     147            [ + ]:          2 :                         }
     148                 :          1 :                         else return res;
     149                 :          4 :                 })
     150            [ + ]:          4 :                 .then((res) => {
     151            [ + ]:          3 :                         if(res !== null){
     152                 :          2 :                                 timestamp = res;
     153                 :          2 :                                 return {activity, username, score, timestamp, rank};
     154            [ + ]:          2 :                         }
     155                 :          1 :                         else return {};
     156                 :          4 :                 });
     157                 :          4 :         }
     158                 :          1 : 
     159                 :          1 :         /**
     160                 :          1 :          * @method
     161                 :          1 :          * @instance
     162                 :          1 :          * @memberof module:redis-leaderboard.LeaderBoard
     163                 :          1 :          * @param    {string} activity
     164                 :          1 :          * @return   {Promise} A Promise object that will fulfill with a leaderboard array of objects.
     165                 :          1 :          * @desc     Retrieve a leaderboard array of objects for such activity.
     166                 :          1 :          * @throws   {Error} Throws an error if the given activity does not exist.
     167                 :          1 :          */
     168            [ + ]:          1 :         getActivity(activity){
     169                 :          2 :                 let actUser = [];
     170                 :          2 :                 let tsKeys = null, tsValues = null;
     171                 :          2 : 
     172                 :          2 :                 return this.client.sendCommand([
     173                 :          2 :                         'KEYS',
     174                 :          2 :                         `${this.keyA}${activity}`,
     175                 :          2 :                 ])
     176            [ + ]:          2 :                 .then((res) => {
     177            [ + ]:          2 :                         if(!res.length){
     178                 :          1 :                                 throw new Error(`no activity '${activity}'`);
     179                 :          1 :                         }
     180                 :          1 :                         return this.client.sendCommand([
     181                 :          1 :                                 'ZREVRANGE',
     182                 :          1 :                                 `${this.keyA}${activity}`,
     183                 :          1 :                                 '0', '-1', 'WITHSCORES'
     184                 :          1 :                         ]);
     185                 :          2 :                 })
     186            [ + ]:          2 :                 .then((res) => {
     187                 :          1 : 
     188            [ + ]:          1 :                         for(let i = 0; i < res.length; i += 2){
     189                 :          2 :                                 let entry = {};
     190                 :          2 :                                 entry.activity  = activity;
     191                 :          2 :                                 entry.username  = res[i];
     192                 :          2 :                                 entry.score     = res[i+1];
     193                 :          2 :                                 entry.timestamp = -1;
     194                 :          2 :                                 entry.rank      = i / 2 + 1;
     195                 :          2 :                                 actUser.push(entry);
     196                 :          2 :                         }
     197                 :          1 : 
     198                 :          1 :                         return this.client.sendCommand([
     199                 :          1 :                                 'KEYS',
     200                 :          1 :                                 `${this.keyT}${activity}*`,
     201                 :          1 :                         ]);
     202                 :          2 :                 })
     203            [ + ]:          2 :                 .then((res) => {
     204                 :          1 :                         tsKeys = res;
     205            [ + ]:          1 :                         return Promise.all(res.map(key =>
     206                 :          2 :                                 this.client.sendCommand([
     207                 :          2 :                                         'HGET', key, 'timestamp'])
     208                 :          1 :                                 ));
     209                 :          2 :                 })
     210            [ + ]:          2 :                 .then((res) => {
     211                 :          1 :                         tsValues = res;
     212                 :          1 : 
     213                 :          1 :                         let tsObj = {};
     214            [ + ]:          1 :                         for(let i = 0; i < tsKeys.length; i++){
     215                 :          2 :                                 tsObj[tsKeys[i].replace(`${this.keyT}`, '')] = tsValues[i];
     216                 :          2 :                         }
     217                 :          1 : 
     218            [ + ]:          1 :                         for(let i = 0; i < actUser.length; i++){
     219                 :          2 :                                 let key = `${actUser[i].activity}:${actUser[i].username}`;
     220                 :          2 :                                 actUser[i].timestamp = tsObj[key];
     221                 :          2 :                         }
     222                 :          1 : 
     223                 :          1 :                         return actUser;
     224                 :          2 :                 });
     225                 :          2 :         }
     226                 :          1 : 
     227                 :          1 :         /**
     228                 :          1 :          * @method
     229                 :          1 :          * @instance
     230                 :          1 :          * @memberof module:redis-leaderboard.LeaderBoard
     231                 :          1 :          * @return   {Promise} A Promise object that will fulfill with a leaderboard array of objects.
     232                 :          1 :          * @desc     Retrieve a leaderboard array of objects for all activities with rank.
     233                 :          1 :          */
     234            [ + ]:          1 :         getActivities(){
     235                 :          1 :                 let actUser = [];
     236                 :          1 :                 let actKeys = null, actValues = null;
     237                 :          1 :                 let tsKeys = null, tsValues = null;
     238                 :          1 : 
     239                 :          1 :                 return this.client.sendCommand([
     240                 :          1 :                         'KEYS',
     241                 :          1 :                         `${this.keyA}*`,
     242                 :          1 :                 ])
     243            [ + ]:          1 :                 .then((res) => {
     244                 :          1 :                         actKeys = res;
     245            [ + ]:          1 :                         return Promise.all(res.map(key =>
     246                 :          2 :                                 this.client.sendCommand([
     247                 :          2 :                                         'ZRANGE',
     248                 :          2 :                                         key,
     249                 :          2 :                                         '0', '-1', 'WITHSCORES'])
     250                 :          1 :                                 )
     251                 :          1 :                         );
     252                 :          1 :                 })
     253            [ + ]:          1 :                 .then((res) => {
     254                 :          1 :                         actValues = res;
     255                 :          1 : 
     256            [ + ]:          1 :                         for(let i = 0; i < actValues.length; i++){
     257            [ + ]:          2 :                                 for(let j = 0; j < actValues[i].length; j += 2){
     258                 :          4 :                                         let entry = {};
     259                 :          4 :                                         entry.activity = actKeys[i].replace(`${this.keyA}`, '');
     260                 :          4 :                                         entry.username = actValues[i][j];
     261                 :          4 :                                         entry.score    = actValues[i][j+1];
     262                 :          4 :                                         entry.timestamp = -1;
     263                 :          4 :                                         entry.rank      = -1;
     264                 :          4 :                                         actUser.push(entry);
     265                 :          4 :                                 }
     266                 :          2 :                         }
     267                 :          1 : 
     268                 :          1 :                         return this.client.sendCommand([
     269                 :          1 :                                 'KEYS',
     270                 :          1 :                                 `${this.keyT}*`,
     271                 :          1 :                         ]);
     272                 :          1 :                 })
     273            [ + ]:          1 :                 .then((res) => {
     274                 :          1 :                         tsKeys = res;
     275            [ + ]:          1 :                         return Promise.all(res.map(key =>
     276                 :          4 :                                 this.client.sendCommand([
     277                 :          4 :                                         'HGET', key, 'timestamp'])
     278                 :          1 :                         ));
     279                 :          1 :                 })
     280            [ + ]:          1 :                 .then((res) => {
     281                 :          1 :                         tsValues = res;
     282                 :          1 : 
     283                 :          1 :                         let tsObj = {};
     284            [ + ]:          1 :                         for(let i = 0; i < tsKeys.length; i++){
     285                 :          4 :                                 tsObj[tsKeys[i].replace(`${this.keyT}`, '')] = tsValues[i];
     286                 :          4 :                         }
     287                 :          1 : 
     288            [ + ]:          1 :                         for(let i = 0; i < actUser.length; i++){
     289                 :          4 :                                 let key = `${actUser[i].activity}:${actUser[i].username}`;
     290                 :          4 :                                 actUser[i].timestamp = tsObj[key];
     291                 :          4 :                         }
     292                 :          1 : 
     293            [ + ]:          1 :                         actUser.sort((a, b) => {
     294            [ - ]:          3 :                                 if(+a.score < +b.score) return 1;
     295            [ - ]:          3 :                                 else if(+a.score > +b.score) return -1;
     296                 :          0 :                                 else if(a.activity < b.activity) return -1;
     297                 :          0 :                                 else if(a.activity > b.activity) return 1;
     298                 :          0 :                                 else if(a.username < b.username) return -1;
     299                 :          0 :                                 else return 1;
     300                 :          1 :                         })
     301            [ + ]:          1 :                         .forEach((e, i, a) => e.rank = i + 1);
     302                 :          1 : 
     303                 :          1 :                         return actUser;
     304                 :          1 :                 });
     305                 :          1 :         }
     306                 :          1 : 
     307                 :          1 :         /**
     308                 :          1 :          * @method
     309                 :          1 :          * @instance
     310                 :          1 :          * @memberof module:redis-leaderboard.LeaderBoard
     311                 :          1 :          * @param    {string} username
     312                 :          1 :          * @return   {Promise} A Promise object that will fulfill with a leaderboard array of objects.
     313                 :          1 :          * @desc     Retrieve a leaderboard array of objects for all activities of the given username.
     314                 :          1 :          */
     315            [ + ]:          1 :         getUserActivities(username){
     316                 :          2 :                 let actUser = [];
     317                 :          2 :                 let actKeys = null, actValues = null;
     318                 :          2 :                 let tsKeys = null, tsValues = null;
     319                 :          2 : 
     320                 :          2 :                 return this.client.sendCommand([
     321                 :          2 :                         'KEYS',
     322                 :          2 :                         `${this.keyT}*:${username}`,
     323                 :          2 :                 ])
     324            [ + ]:          2 :                 .then((res) => {
     325                 :          2 :                         tsKeys = res;
     326                 :          2 : 
     327            [ + ]:          2 :                         return Promise.all(res.map(key =>
     328                 :          2 :                                 this.client.sendCommand([
     329                 :          2 :                                         'HGET', key, 'timestamp'])
     330                 :          2 :                         ));
     331                 :          2 :                 })
     332            [ + ]:          2 :                 .then((res) => {
     333                 :          2 :                         tsValues = res;
     334                 :          2 : 
     335                 :          2 :                         actKeys = tsKeys
     336            [ + ]:          2 :                                 .map(key => key.replace(`:${username}`, ''))
     337            [ + ]:          2 :                                 .map(key => key.replace(`${this.keyT}`, `${this.keyA}`));
     338                 :          2 : 
     339            [ + ]:          2 :                         return Promise.all(actKeys.map(key =>
     340                 :          2 :                                 this.client.sendCommand([
     341                 :          2 :                                         'ZRANGE',
     342                 :          2 :                                         key,
     343                 :          2 :                                         '0', '-1', 'WITHSCORES'])
     344                 :          2 :                                 )
     345                 :          2 :                         );
     346                 :          2 :                 })
     347            [ + ]:          2 :                 .then((res) => {
     348                 :          2 :                         for(let i = 0; i < res.length; i++){
     349            [ + ]:          2 :                                 for(let j = 0; j < res[i].length; j += 2){
     350            [ + ]:          4 :                                         if(res[i][j] === username){
     351                 :          2 :                                                 res[i] = [res[i][j], res[i][j+1]];
     352                 :          2 :                                                 break;
     353                 :          2 :                                         }
     354                 :          4 :                                 }
     355                 :          2 :                         }
     356                 :          2 :                         actValues = res;
     357                 :          2 :                         for(let i = 0; i < actValues.length; i++){
     358                 :          2 :                                 let entry = {};
     359                 :          2 :                                 entry.activity = actKeys[i].replace(`${this.keyA}`, '');
     360                 :          2 :                                 entry.username = actValues[i][0];
     361                 :          2 :                                 entry.score    = actValues[i][1];
     362                 :          2 :                                 entry.timestamp = -1;
     363                 :          2 :                                 entry.rank      = -1;
     364                 :          2 :                                 actUser.push(entry);
     365                 :          2 :                         }
     366                 :          2 : 
     367                 :          2 :                         let tsObj = {};
     368                 :          2 :                         for(let i = 0; i < tsKeys.length; i++){
     369                 :          2 :                                 tsObj[tsKeys[i].replace(`${this.keyT}`, '')] = tsValues[i];
     370                 :          2 :                         }
     371                 :          2 : 
     372                 :          2 :                         for(let i = 0; i < actUser.length; i++){
     373                 :          2 :                                 let key = `${actUser[i].activity}:${actUser[i].username}`;
     374                 :          2 :                                 actUser[i].timestamp = tsObj[key];
     375                 :          2 :                         }
     376                 :          2 : 
     377            [ + ]:          2 :                         actUser.sort((a, b) => {
     378            [ - ]:          1 :                                 if(+a.score < +b.score) return 1;
     379                 :          0 :                                 else if(+a.score > +b.score) return -1;
     380                 :          0 :                                 else if(a.activity < b.activity) return -1;
     381                 :          0 :                                 else if(a.activity > b.activity) return 1;
     382                 :          0 :                                 else return 1;
     383                 :          2 :                         })
     384            [ + ]:          2 :                         .forEach((e, i, a) => e.rank = i + 1);
     385                 :          2 : 
     386                 :          2 :                         return actUser;
     387                 :          2 :                 });
     388                 :          2 :         }
     389                 :          1 : 
     390                 :          1 :         /**
     391                 :          1 :          * @method
     392                 :          1 :          * @instance
     393                 :          1 :          * @memberof module:redis-leaderboard.LeaderBoard
     394                 :          1 :          * @param    {string} activity.
     395                 :          1 :          * @param    {number} n.
     396                 :          1 :          * @return   {Promise} A Promise object that will fulfill with a leaderboard array of objects.
     397                 :          1 :          * @desc     Retrieve a leaderboard array of objects for n top users of the given activity.
     398                 :          1 :          */
     399            [ + ]:          1 :         getActivityTopUsers(activity, n){
     400                 :          5 : 
     401                 :          5 :                 let tsKeys = null, tsValues = null;
     402                 :          5 : 
     403                 :          5 :                 return this.client.sendCommand([
     404                 :          5 :                         'KEYS',
     405                 :          5 :                         `${this.keyT}${activity}*`
     406                 :          5 :                 ])
     407            [ + ]:          5 :                 .then((res) => {
     408                 :          5 :                         tsKeys = res;
     409            [ + ]:          5 :                         return Promise.all(res.map(key =>
     410                 :          8 :                                 this.client.sendCommand([
     411                 :          8 :                                         'HGET', key, 'timestamp'])
     412                 :          5 :                                 ));
     413                 :          5 :                 })
     414            [ + ]:          5 :                 .then((res) => {
     415                 :          5 :                         tsValues = res;
     416                 :          5 :                         return this.client.sendCommand([
     417                 :          5 :                                 'ZREVRANGE',
     418                 :          5 :                                 `${this.keyA}${activity}`, String(0), String(n - 1), 'WITHSCORES']);
     419                 :          5 :                 })
     420            [ + ]:          5 :                 .then((res) => {
     421                 :          5 : 
     422                 :          5 :                         let tsObj = {};
     423            [ + ]:          5 :                         for(let i = 0; i < tsKeys.length; i++){
     424                 :          8 :                                 tsObj[tsKeys[i].replace(`${this.keyT}`, '').replace(`${activity}:`, '')] = tsValues[i];
     425                 :          8 :                         }
     426                 :          5 : 
     427                 :          5 :                         const actUser = [];
     428            [ + ]:          5 :                         for(let i = 0, rank = 1 ; i < res.length ; i += 2, rank++){
     429                 :          7 :                                 actUser.push({ activity, username: res[i], score: `${res[i + 1]}`, timestamp: tsObj[res[i]], rank: rank });
     430                 :          7 :                         }
     431                 :          5 :                         return actUser;
     432                 :          5 :                 });
     433                 :          5 :         }
     434                 :          1 : }

Generated by: LCOV version 1.14