Skip to content

Commit ab8d3c5

Browse files
fix(timeline): fix refresh bug
* feat(timeline): for-you v2 (under test) * fix(timeline): circular dependency with bg module * fix(timeline): bug * feat(timeline): v2 done with seen property and interests based * fix(test): unit tests * fix(ci): fix package-lock.json * fix(timeline): bug in refresh sign up * fix(test): mig remove * fix(timeline): fix dependencies * fix(timeline): bug in refresh sign up --------- Co-authored-by: Alyaa Ali <eissaalyaa@gmail.com>
1 parent a17d4ba commit ab8d3c5

File tree

4 files changed

+71
-19
lines changed

4 files changed

+71
-19
lines changed

src/timeline/services/foryou/for-you.service.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,9 @@ export class ForyouService {
3030
cursor?: string, // Keep for API compatibility but not used
3131
limit: number = 20
3232
): Promise<{
33-
// data: ScoredCandidateDTO[];
3433
data: TweetResponseDTO[];
3534
pagination: { next_cursor: string | null; has_more: boolean };
3635
}> {
37-
// Get or create cursor for this user
3836
let timeline_cursor = await this.timeline_cursor_repository.findOne({
3937
where: { user_id },
4038
});
@@ -66,7 +64,6 @@ export class ForyouService {
6664
);
6765

6866
// Fallback: Fetch tweets directly from candidates service
69-
// This handles the case where frontend calls immediately after assigning interests
7067
const candidates = await this.timeline_candidates_service.getCandidates(
7168
user_id,
7269
new Set(), // No exclusions for fresh start
@@ -88,7 +85,7 @@ export class ForyouService {
8885
);
8986
return {
9087
data: fallback_tweets,
91-
pagination: { next_cursor: null, has_more: false },
88+
pagination: { next_cursor: 'next', has_more: true },
9289
};
9390
}
9491

src/timeline/services/timeline-candidates.service.spec.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,17 @@ import { Repository } from 'typeorm';
66
import { UserInterests } from 'src/user/entities/user-interests.entity';
77
import { TweetCategory } from 'src/tweets/entities/tweet-category.entity';
88
import { Tweet } from 'src/tweets/entities/tweet.entity';
9+
import { Category } from 'src/category/entities/category.entity';
10+
import { InitTimelineQueueJobService } from 'src/background-jobs/timeline/timeline.service';
911

1012
describe('TimelineCandidatesService', () => {
1113
let service: TimelineCandidatesService;
1214
let user_interests_repository: jest.Mocked<Repository<UserInterests>>;
1315
let tweet_category_repository: jest.Mocked<Repository<TweetCategory>>;
1416
let tweet_repository: jest.Mocked<Repository<Tweet>>;
17+
let category_repository: jest.Mocked<Repository<Category>>;
1518
let config_service: jest.Mocked<ConfigService>;
19+
let init_timeline_queue_job_service: jest.Mocked<InitTimelineQueueJobService>;
1620

1721
const mock_user_id = 'user-123';
1822
const mock_user_interests = [
@@ -48,6 +52,8 @@ describe('TimelineCandidatesService', () => {
4852
provide: getRepositoryToken(UserInterests),
4953
useValue: {
5054
find: jest.fn(),
55+
create: jest.fn(),
56+
save: jest.fn(),
5157
},
5258
},
5359
{
@@ -62,6 +68,22 @@ describe('TimelineCandidatesService', () => {
6268
createQueryBuilder: jest.fn(),
6369
},
6470
},
71+
{
72+
provide: getRepositoryToken(Category),
73+
useValue: {
74+
createQueryBuilder: jest.fn(() => ({
75+
orderBy: jest.fn().mockReturnThis(),
76+
limit: jest.fn().mockReturnThis(),
77+
getMany: jest.fn().mockResolvedValue([]),
78+
})),
79+
},
80+
},
81+
{
82+
provide: InitTimelineQueueJobService,
83+
useValue: {
84+
queueInitTimelineQueue: jest.fn().mockResolvedValue(undefined),
85+
},
86+
},
6587
{
6688
provide: ConfigService,
6789
useValue: {
@@ -78,6 +100,8 @@ describe('TimelineCandidatesService', () => {
78100
user_interests_repository = module.get(getRepositoryToken(UserInterests));
79101
tweet_category_repository = module.get(getRepositoryToken(TweetCategory));
80102
tweet_repository = module.get(getRepositoryToken(Tweet));
103+
category_repository = module.get(getRepositoryToken(Category));
104+
init_timeline_queue_job_service = module.get(InitTimelineQueueJobService);
81105
config_service = module.get(ConfigService);
82106
});
83107

src/timeline/services/timeline-candidates.service.ts

Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ import { Repository } from 'typeorm';
55
import { UserInterests } from 'src/user/entities/user-interests.entity';
66
import { TweetCategory } from 'src/tweets/entities/tweet-category.entity';
77
import { Tweet } from 'src/tweets/entities/tweet.entity';
8+
import { Category } from 'src/category/entities/category.entity';
9+
import { InitTimelineQueueJobService } from 'src/background-jobs/timeline/timeline.service';
10+
import { JOB_DELAYS } from 'src/background-jobs/constants/queue.constants';
811

912
export interface ICandidateTweet {
1013
tweet_id: string;
@@ -25,40 +28,36 @@ export class TimelineCandidatesService {
2528
private readonly tweet_category_repository: Repository<TweetCategory>,
2629
@InjectRepository(Tweet)
2730
private readonly tweet_repository: Repository<Tweet>,
28-
private readonly config_service: ConfigService
31+
@InjectRepository(Category)
32+
private readonly category_repository: Repository<Category>,
33+
private readonly config_service: ConfigService,
34+
private readonly init_timeline_queue_job_service: InitTimelineQueueJobService
2935
) {
3036
this.tweet_freshness_days = this.config_service.get<number>(
3137
'TIMELINE_TWEET_FRESHNESS_DAYS',
3238
7
3339
);
3440

35-
this.LIMIT_FACTOR = 500; // Factor to over-fetch for filtering
41+
this.LIMIT_FACTOR = 500;
3642
}
3743

38-
/**
39-
* Get candidate tweets based on user's interests
40-
* @param user_id User ID
41-
* @param excluded_tweet_ids Tweet IDs to exclude (already seen)
42-
* @param limit Maximum number of candidates to return
43-
* @returns Array of candidate tweets
44-
*/
4544
async getCandidates(
4645
user_id: string,
4746
excluded_tweet_ids: Set<string>,
4847
limit: number
4948
): Promise<ICandidateTweet[]> {
50-
// console.log(
51-
// `[Candidates] Getting ${limit} candidates for user ${user_id}, excluding ${excluded_tweet_ids.size} tweets`
52-
// );
5349
const user_interests = await this.user_interests_repository.find({
5450
where: { user_id },
5551
order: { score: 'DESC' },
5652
});
57-
// console.log(`[Candidates] Found ${user_interests.length} interests for user ${user_id}`);
5853

5954
if (user_interests.length === 0) {
60-
console.log(`[Candidates] No interests found, using random fallback`);
61-
// Fallback: Get random fresh tweets if user has no interests
55+
console.log(`[Candidates] No interests found, assigning 3 random interests`);
56+
// No interests means that the user makes a refresh before inserting their interests
57+
// Assign 3 random interests and trigger the init timeline queue job
58+
await this.assignRandomInterests(user_id);
59+
await this.init_timeline_queue_job_service.queueInitTimelineQueue({ user_id });
60+
// for now, return random tweets while the background job processes
6261
return this.getRandomFreshTweets(user_id, excluded_tweet_ids, limit);
6362
}
6463

@@ -299,4 +298,34 @@ export class TimelineCandidatesService {
299298

300299
return candidates;
301300
}
301+
302+
private async assignRandomInterests(user_id: string): Promise<void> {
303+
try {
304+
const random_categories = await this.category_repository
305+
.createQueryBuilder('category')
306+
.orderBy('RANDOM()')
307+
.limit(3)
308+
.getMany();
309+
310+
if (random_categories.length === 0) {
311+
console.error(`[Candidates] No categories available to assign`);
312+
return;
313+
}
314+
315+
const user_interests = random_categories.map((category) =>
316+
this.user_interests_repository.create({
317+
user_id,
318+
category_id: String(category.id),
319+
score: 100,
320+
})
321+
);
322+
323+
await this.user_interests_repository.save(user_interests);
324+
} catch (error) {
325+
console.error(
326+
`[Candidates] Error assigning random interests to user ${user_id}:`,
327+
error
328+
);
329+
}
330+
}
302331
}

src/timeline/timeline.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { TimelineRedisService } from './services/timeline-redis.service';
1515
import { TimelineCandidatesService } from './services/timeline-candidates.service';
1616
import { BackgroundJobsModule } from 'src/background-jobs/background-jobs.module';
1717
import { RedisModuleConfig } from 'src/redis/redis.module';
18+
import { Category } from 'src/category/entities';
1819

1920
@Module({
2021
imports: [
@@ -26,6 +27,7 @@ import { RedisModuleConfig } from 'src/redis/redis.module';
2627
TweetCategory,
2728
UserInterests,
2829
UserTimelineCursor,
30+
Category,
2931
]),
3032
forwardRef(() => BackgroundJobsModule),
3133
RedisModuleConfig,

0 commit comments

Comments
 (0)