Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 24 additions & 5 deletions src/post/services/hashtag-trends.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,14 @@ export class HashtagTrendService {

if (trending.length === 0) {
this.logger.warn(`No trending data in Redis for ${category}, falling back to DB`);
return await this.getTrendingFromDB(limit, category);
const dbResults = await this.getTrendingFromDB(limit, category);

if (dbResults.length > 0) {
await this.redisService.setJSON(cacheKey, dbResults, this.CACHE_TTL);
this.logger.debug(`Cached ${dbResults.length} DB results for ${category}`);
}

return dbResults;
}

this.failureCount = 0;
Expand Down Expand Up @@ -326,7 +333,6 @@ export class HashtagTrendService {
return trends.map((trend) => ({
tag: `#${trend.hashtag.tag}`,
totalPosts: trend.post_count_7d,
score: trend.trending_score,
}));
} catch (error) {
this.logger.error('Failed to get trending from DB:', error);
Expand Down Expand Up @@ -404,16 +410,29 @@ export class HashtagTrendService {
}

private async determineCategories(event: PostCreatedEvent): Promise<TrendCategory[]> {
const categories: Set<TrendCategory> = new Set([TrendCategory.GENERAL]);
const categories: Set<TrendCategory> = new Set();

categories.add(TrendCategory.GENERAL);

if (event.interestSlug) {
for (const [category, slugs] of Object.entries(CATEGORY_TO_INTERESTS)) {
if (slugs.includes(event.interestSlug)) {
if (category === TrendCategory.GENERAL || category === TrendCategory.PERSONALIZED) {
continue;
}
Comment on lines +419 to +421
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic for excluding GENERAL and PERSONALIZED is redundant since GENERAL is explicitly added at line 415, and PERSONALIZED should not be in CATEGORY_TO_INTERESTS. This filtering suggests a potential misunderstanding of the data structure. Consider validating that CATEGORY_TO_INTERESTS doesn't contain these categories, or document why this defensive check is necessary.

Suggested change
if (category === TrendCategory.GENERAL || category === TrendCategory.PERSONALIZED) {
continue;
}
// Assumption: CATEGORY_TO_INTERESTS does not contain GENERAL or PERSONALIZED

Copilot uses AI. Check for mistakes.
if (slugs.length > 0 && slugs.includes(event.interestSlug)) {
categories.add(category as TrendCategory);
this.logger.debug(
`Post ${event.postId} with interest '${event.interestSlug}' mapped to category '${category}'`
);
}
}
}

return Array.from(categories);
const result = Array.from(categories);
this.logger.debug(
`Post ${event.postId} will be tracked in categories: ${result.join(', ')}`
);

return result;
}
}
6 changes: 2 additions & 4 deletions src/post/services/personalized-trends.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export class PersonalizedTrendsService {
async getPersonalizedTrending(
userId: number,
limit: number = 10,
): Promise<Array<{ tag: string; totalPosts: number; score: number; categories: string[] }>> {
): Promise<Array<{ tag: string; totalPosts: number; }>> {
const cacheKey = `personalized:trending:${userId}:${limit}`;
const cached = await this.redisService.getJSON<any[]>(cacheKey);
if (cached && cached.length > 0) {
Expand All @@ -53,6 +53,7 @@ export class PersonalizedTrendsService {
const userInterests = await this.usersService.getUserInterests(userId);
const interestSlugs = userInterests.map((ui) => ui.slug);
const categories = this.mapInterestsToCategories(interestSlugs);

if (categories.length === 0) {
this.logger.debug(`User ${userId} has no interests, falling back to GENERAL`);
return await this.getTrendingForCategory(TrendCategory.GENERAL, limit);
Expand All @@ -71,7 +72,6 @@ export class PersonalizedTrendsService {
this.logger.warn(`No personalized trends for user ${userId}, using GENERAL`);
return await this.getTrendingForCategory(TrendCategory.GENERAL, limit);
}

const results = await Promise.all(
combinedTrends.map(async (trend) => {
let metadata = await this.redisTrendingService.getHashtagMetadata(
Expand Down Expand Up @@ -103,8 +103,6 @@ export class PersonalizedTrendsService {
return {
tag: `#${metadata.tag}`,
totalPosts: counts.count7d,
score: trend.combinedScore,
categories: trend.categories,
};
}),
);
Expand Down
41 changes: 26 additions & 15 deletions src/post/services/post.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -580,21 +580,32 @@ export class PostService {

// Emit post.created event for real-time hashtag tracking
if (hashtagIds.length > 0) {
let interestSlug: string | undefined;
if (post.interest_id) {
const interest = await this.prismaService.interest.findUnique({
where: { id: post.interest_id },
select: { slug: true },
});
interestSlug = interest?.slug;
}
this.eventEmitter.emit('post.created', {
postId: post.id,
userId: post.user_id,
hashtagIds,
interestSlug,
timestamp: post.created_at.getTime(),
});
setTimeout(async () => {
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using setTimeout with a hardcoded delay of 1500ms is a fragile solution that creates a race condition. If the database update takes longer than 1.5 seconds, the interest_id may still be null. Consider using a more reliable approach such as refetching the post within the same transaction, using database triggers, or implementing a retry mechanism with exponential backoff.

Copilot uses AI. Check for mistakes.
try {
let interestSlug: string | undefined;
const updatedPost = await this.prismaService.post.findUnique({
where: { id: post.id },
select: { interest_id: true },
});

if (updatedPost?.interest_id) {
const interest = await this.prismaService.interest.findUnique({
where: { id: updatedPost.interest_id },
select: { slug: true },
});
interestSlug = interest?.slug;
}
this.eventEmitter.emit('post.created', {
postId: post.id,
userId: post.user_id,
hashtagIds,
interestSlug,
timestamp: post.created_at.getTime(),
});
} catch (error) {
console.error('Failed to emit post.created event:', error);
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Direct console.error usage is inconsistent with the logging pattern typically used in services. Consider using a logger instance (e.g., this.logger.error) for consistent log formatting and level management.

Copilot uses AI. Check for mistakes.
}
}, 1500);
}

// Update parent post stats cache if this is a reply or quote
Expand Down
Loading