Skip to content

Commit ca5cb9c

Browse files
authored
Merge pull request #151 from game-node-app/dev
Improvements to Trending Games results
2 parents 3d0043b + 3d0332c commit ca5cb9c

File tree

4 files changed

+46
-29
lines changed

4 files changed

+46
-29
lines changed

server_swagger.json

+1-1
Large diffs are not rendered by default.

src/statistics/entity/user-view.entity.ts

+2
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ export class UserView {
4444
nullable: true,
4545
})
4646
gameStatistics: GameStatistics | null;
47+
@Column()
48+
gameStatisticsId: number | null;
4749
@ManyToOne(() => ReviewStatistics, (rs) => rs.views, {
4850
onDelete: "CASCADE",
4951
nullable: true,

src/statistics/game-statistics.service.ts

+39-24
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ import { getPreviousDate } from "./statistics.utils";
2323
import { hours } from "@nestjs/throttler";
2424
import { GameRepositoryService } from "../game/game-repository/game-repository.service";
2525
import { Cache } from "@nestjs/cache-manager";
26-
import { MATURE_THEME_ID } from "../game/game-filter/game-filter.constants";
2726
import isEmptyObject from "../utils/isEmptyObject";
27+
import { MATURE_THEME_ID } from "../game/game-filter/game-filter.constants";
2828

2929
@Injectable()
3030
export class GameStatisticsService implements StatisticsService {
@@ -180,12 +180,12 @@ export class GameStatisticsService implements StatisticsService {
180180
}
181181

182182
async findTrending(
183-
data: FindStatisticsTrendingGamesDto,
183+
dto: FindStatisticsTrendingGamesDto,
184184
): Promise<TPaginationData<GameStatistics>> {
185-
const { period, criteria, offset, limit } = data;
185+
const { period, criteria, offset, limit } = dto;
186186
const offsetToUse = offset || 0;
187-
// We save up to this N statistics entities on cache to improve load performance.
188-
const fixedStatisticsLimit = 25000;
187+
188+
const FIXED_STATISTICS_LIMIT = 50000;
189189
// User supplied limit
190190
const limitToUse = limit || 20;
191191
const minusDays = StatisticsPeriodToMinusDays[period];
@@ -194,31 +194,46 @@ export class GameStatisticsService implements StatisticsService {
194194
let statistics = await this.getCachedStatistics(period);
195195

196196
if (statistics == undefined) {
197-
const queryBuilder =
198-
this.gameStatisticsRepository.createQueryBuilder("s");
199-
200197
/**
201198
* Made with query builder, so we can further optimize the query
202199
*/
200+
const queryBuilder =
201+
this.gameStatisticsRepository.createQueryBuilder("gs");
202+
203+
const userViewSubQuery = this.userViewRepository
204+
.createQueryBuilder("uv")
205+
.select("uv.gameStatisticsId, COUNT(uv.id) AS total")
206+
.where("uv.gameStatisticsId IS NOT NULL")
207+
.groupBy("uv.gameStatisticsId");
208+
209+
if (period !== StatisticsPeriod.ALL) {
210+
userViewSubQuery.andWhere("uv.createdAt >= :viewsStartDate");
211+
}
212+
203213
const query = queryBuilder
204-
.select()
205-
.leftJoin(UserView, `uv`, `uv.gameStatisticsId = s.id`)
206-
.where(`(uv.createdAt >= :uvDate OR s.viewsCount = 0)`, {
207-
uvDate: viewsStartDate,
208-
})
209-
// Excludes games with mature theme
210-
.andWhere(
211-
`NOT EXISTS (SELECT 1 FROM game_themes_game_theme AS gtgt WHERE gtgt.gameId = s.gameId
214+
.addSelect("IFNULL(in_period.total, 0) AS views_in_period")
215+
.leftJoin(
216+
`(${userViewSubQuery.getQuery()})`,
217+
"in_period",
218+
"in_period.gameStatisticsId = gs.id",
219+
)
220+
.where(
221+
// Excludes games with mature theme
222+
`NOT EXISTS (SELECT 1 FROM game_themes_game_theme AS gtgt WHERE gtgt.gameId = gs.gameId
212223
AND gtgt.gameThemeId = :excludedThemeId)`,
213-
{
214-
excludedThemeId: MATURE_THEME_ID,
215-
},
216224
)
217-
.addOrderBy(`s.viewsCount`, `DESC`)
218-
.skip(0)
219-
.take(fixedStatisticsLimit);
225+
.orderBy("views_in_period", "DESC")
226+
.addOrderBy("gs.viewsCount", "DESC")
227+
.limit(FIXED_STATISTICS_LIMIT);
228+
229+
query.setParameters({
230+
viewsStartDate: viewsStartDate,
231+
excludedThemeId: MATURE_THEME_ID,
232+
});
220233

221234
statistics = await query.getMany();
235+
// Storing the entire table takes roughly ~16mb in Redis.
236+
// 16mb * 7 = ~112mb to store statistics for all periods
222237
this.setCachedStatistics(period, statistics);
223238
}
224239

@@ -228,15 +243,15 @@ export class GameStatisticsService implements StatisticsService {
228243
offsetToUse,
229244
offsetToUse + limitToUse,
230245
);
231-
return [slicedStatistics, fixedStatisticsLimit];
246+
return [slicedStatistics, FIXED_STATISTICS_LIMIT];
232247
}
233248

234249
const gameIds = statistics.map((s) => s.gameId);
235250
const games = await this.gameRepositoryService.findAllIdsWithFilters({
236251
...criteria,
237252
ids: gameIds,
238253
// We need to return all entities to maintain pagination order
239-
limit: fixedStatisticsLimit,
254+
limit: FIXED_STATISTICS_LIMIT,
240255
offset: 0,
241256
});
242257
const totalAvailableGames = games.length;

src/utils/cacheable.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,10 @@ export function Cacheable(
3232

3333
const cache: Cache = this.cacheManager;
3434

35-
if (!cache) {
36-
throw new Error(
37-
"Cannot use Cacheable() decorator without injecting the cache manager.",
38-
);
35+
// This can happen if "cacheManager" is not defined in class, or if we "this" is not the actual class
36+
// reference.
37+
if (cache == undefined) {
38+
return originalMethod.apply(this, args);
3939
}
4040

4141
// Try to get cached data

0 commit comments

Comments
 (0)