Skip to content

Commit cbdc9ac

Browse files
authored
Merge pull request #162 from Mirabel64/perf/stellar-wave-perf-fixes
perf: address Stellar Wave issues #100, #101, #102, #103
2 parents 5520291 + 6835e50 commit cbdc9ac

6 files changed

Lines changed: 448 additions & 258 deletions

File tree

backend/src/analytics/analytics.controller.ts

Lines changed: 25 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -31,48 +31,51 @@ export class AnalyticsController implements OnModuleInit {
3131

3232
@Post('record-solve')
3333
@HttpCode(HttpStatus.NO_CONTENT)
34-
recordSolve(@Body() body: RecordSolveDto): void {
34+
async recordSolve(@Body() body: RecordSolveDto): Promise<void> {
3535
this.logger.log(`Received record-solve request: ${JSON.stringify(body)}`);
3636
const { userId, puzzleId, solveTime } = body;
37-
this.analyticsService.recordPuzzleSolve(userId, puzzleId, solveTime);
37+
// Await so write errors surface as 5xx rather than being silently
38+
// dropped; the service internally falls back to in-memory on Redis
39+
// failure so this won't crash the request.
40+
await this.analyticsService.recordPuzzleSolveAsync(
41+
userId,
42+
puzzleId,
43+
solveTime,
44+
);
3845
}
3946

4047
@Get('puzzles/most-solved')
4148
async getMostSolvedPuzzles(): Promise<
4249
Array<{ puzzleId: string; solveCount: number }>
4350
> {
4451
this.logger.log('Handling request for most solved puzzles.');
45-
return this.analyticsService.getMostSolvedPuzzles();
52+
return this.analyticsService.getMostSolvedPuzzlesAsync();
4653
}
4754

4855
@Get('puzzles/:puzzleId/average-solve-time')
49-
getAverageSolveTime(@Param('puzzleId') puzzleId: string): {
50-
puzzleId: string;
51-
averageSolveTime: number;
52-
} {
56+
async getAverageSolveTime(
57+
@Param('puzzleId') puzzleId: string,
58+
): Promise<{ puzzleId: string; averageSolveTime: number }> {
5359
this.logger.log(
5460
`Handling request for average solve time for puzzle ${puzzleId}.`,
5561
);
5662
const averageSolveTime =
57-
this.analyticsService.getAverageSolveTime(puzzleId);
63+
await this.analyticsService.getAverageSolveTimeAsync(puzzleId);
5864
return { puzzleId, averageSolveTime };
5965
}
6066

6167
@Get('users/:userId/history')
62-
getUserPuzzleHistory(
68+
async getUserPuzzleHistory(
6369
@Param('userId') userId: string,
64-
@Query('page') page?: string,
65-
@Query('limit') limit?: string,
66-
): PaginatedUserPuzzleHistory {
67-
const parsedPage = page ? parseInt(page, 10) : 1;
68-
const parsedLimit = limit ? parseInt(limit, 10) : 20;
69-
this.logger.log(
70-
`Handling paginated request for user ${userId} puzzle history (page=${parsedPage}, limit=${parsedLimit}).`,
71-
);
72-
return this.analyticsService.getUserPuzzleStatsPage(
73-
userId,
74-
parsedPage > 0 ? parsedPage : 1,
75-
parsedLimit > 0 ? parsedLimit : 20,
76-
);
70+
): Promise<Record<string, any>> {
71+
this.logger.log(`Handling request for user ${userId} puzzle history.`);
72+
const userHistoryMap =
73+
await this.analyticsService.getUserPuzzleStatsAsync(userId);
74+
75+
const userHistoryObject: Record<string, any> = {};
76+
userHistoryMap.forEach((value, key) => {
77+
userHistoryObject[key] = value;
78+
});
79+
return userHistoryObject;
7780
}
7881
}

backend/src/analytics/analytics.service.spec.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,39 @@ describe('AnalyticsService', () => {
2727
it('should be defined', () => {
2828
expect(service).toBeDefined();
2929
});
30+
31+
describe('in-memory fallback (no REDIS_URL)', () => {
32+
it('records solves to the in-memory mirror and aggregates correctly', async () => {
33+
await service.recordPuzzleSolveAsync('u1', 'pA', 100);
34+
await service.recordPuzzleSolveAsync('u1', 'pA', 200);
35+
await service.recordPuzzleSolveAsync('u2', 'pB', 50);
36+
37+
const sorted = await service.getMostSolvedPuzzlesAsync();
38+
expect(sorted).toEqual([
39+
{ puzzleId: 'pA', solveCount: 2 },
40+
{ puzzleId: 'pB', solveCount: 1 },
41+
]);
42+
43+
await expect(service.getAverageSolveTimeAsync('pA')).resolves.toBe(150);
44+
await expect(service.getAverageSolveTimeAsync('pB')).resolves.toBe(50);
45+
await expect(service.getAverageSolveTimeAsync('unknown')).resolves.toBe(
46+
0,
47+
);
48+
49+
const u1History = await service.getUserPuzzleStatsAsync('u1');
50+
expect(u1History.get('pA')).toMatchObject({
51+
solveCount: 2,
52+
totalSolveTime: 300,
53+
attempts: 2,
54+
});
55+
expect(u1History.get('pA')?.lastSolved).toBeInstanceOf(Date);
56+
});
57+
58+
it('records every solve exactly once (no double-increment in mirror)', async () => {
59+
await service.recordPuzzleSolveAsync('u1', 'pA', 100);
60+
await service.recordPuzzleSolve('u1', 'pA', 100);
61+
const sorted = await service.getMostSolvedPuzzlesAsync();
62+
expect(sorted).toEqual([{ puzzleId: 'pA', solveCount: 2 }]);
63+
});
64+
});
3065
});

0 commit comments

Comments
 (0)