This commit is contained in:
2025-12-14 18:11:59 +01:00
parent acc15e8d8d
commit 217337862c
91 changed files with 5919 additions and 1999 deletions

View File

@@ -1,4 +1,5 @@
import type { AsyncUseCase } from '@gridpilot/shared/application';
import type { ILogger } from '../../../shared/src/logging/ILogger';
import type { ISocialGraphRepository } from '../../domain/repositories/ISocialGraphRepository';
import type { CurrentUserSocialDTO } from '../dto/CurrentUserSocialDTO';
import type { FriendDTO } from '../dto/FriendDTO';
@@ -22,33 +23,46 @@ export class GetCurrentUserSocialUseCase
constructor(
private readonly socialGraphRepository: ISocialGraphRepository,
public readonly presenter: ICurrentUserSocialPresenter,
private readonly logger: ILogger,
) {}
async execute(params: GetCurrentUserSocialParams): Promise<void> {
const { driverId } = params;
this.logger.debug('GetCurrentUserSocialUseCase: Starting execution', { params });
try {
const { driverId } = params;
const friendsDomain = await this.socialGraphRepository.getFriends(driverId);
this.logger.debug(`GetCurrentUserSocialUseCase: Fetching friends for driverId: ${driverId}`);
const friendsDomain = await this.socialGraphRepository.getFriends(driverId);
this.logger.debug('GetCurrentUserSocialUseCase: Successfully fetched friends from social graph repository', { friendsCount: friendsDomain.length });
if (friendsDomain.length === 0) {
this.logger.warn(`GetCurrentUserSocialUseCase: No friends found for driverId: ${driverId}`);
}
const friends: FriendDTO[] = friendsDomain.map((friend) => ({
driverId: friend.id,
displayName: friend.name,
avatarUrl: '',
isOnline: false,
lastSeen: new Date(),
}));
const friends: FriendDTO[] = friendsDomain.map((friend) => ({
driverId: friend.id,
displayName: friend.name,
avatarUrl: '',
isOnline: false,
lastSeen: new Date(),
}));
const currentUser: CurrentUserSocialDTO = {
driverId,
displayName: '',
avatarUrl: '',
countryCode: '',
};
const currentUser: CurrentUserSocialDTO = {
driverId,
displayName: '',
avatarUrl: '',
countryCode: '',
};
const viewModel: CurrentUserSocialViewModel = {
currentUser,
friends,
};
const viewModel: CurrentUserSocialViewModel = {
currentUser,
friends,
};
this.presenter.present(viewModel);
this.presenter.present(viewModel);
this.logger.info('GetCurrentUserSocialUseCase: Successfully presented current user social data');
} catch (error) {
this.logger.error('GetCurrentUserSocialUseCase: Error during execution', { error });
throw error;
}
}
}

View File

@@ -6,6 +6,7 @@ import type {
IUserFeedPresenter,
UserFeedViewModel,
} from '../presenters/ISocialPresenters';
import type { ILogger } from '../../../shared/src/logging/ILogger';
export interface GetUserFeedParams {
driverId: string;
@@ -17,18 +18,30 @@ export class GetUserFeedUseCase
constructor(
private readonly feedRepository: IFeedRepository,
public readonly presenter: IUserFeedPresenter,
private readonly logger: ILogger,
) {}
async execute(params: GetUserFeedParams): Promise<void> {
const { driverId, limit } = params;
const items = await this.feedRepository.getFeedForDriver(driverId, limit);
const dtoItems = items.map(mapFeedItemToDTO);
this.logger.debug('Executing GetUserFeedUseCase', { driverId, limit });
const viewModel: UserFeedViewModel = {
items: dtoItems,
};
try {
const items = await this.feedRepository.getFeedForDriver(driverId, limit);
this.logger.info('Successfully retrieved user feed', { driverId, itemCount: items.length });
if (items.length === 0) {
this.logger.warn(`No feed items found for driverId: ${driverId}`);
}
const dtoItems = items.map(mapFeedItemToDTO);
this.presenter.present(viewModel);
const viewModel: UserFeedViewModel = {
items: dtoItems,
};
this.presenter.present(viewModel);
} catch (error) {
this.logger.error('Failed to retrieve user feed', error);
throw error; // Re-throw the error so it can be handled upstream
}
}
}

View File

@@ -2,6 +2,7 @@ import type { Driver } from '@gridpilot/racing/domain/entities/Driver';
import type { FeedItem } from '@gridpilot/social/domain/types/FeedItem';
import type { IFeedRepository } from '@gridpilot/social/domain/repositories/IFeedRepository';
import type { ISocialGraphRepository } from '@gridpilot/social/domain/repositories/ISocialGraphRepository';
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
export type Friendship = {
driverId: string;
@@ -18,89 +19,130 @@ export class InMemoryFeedRepository implements IFeedRepository {
private readonly feedEvents: FeedItem[];
private readonly friendships: Friendship[];
private readonly driversById: Map<string, Driver>;
private readonly logger: ILogger;
constructor(seed: RacingSeedData) {
constructor(logger: ILogger, seed: RacingSeedData) {
this.logger = logger;
this.logger.info('InMemoryFeedRepository initialized.');
this.feedEvents = seed.feedEvents;
this.friendships = seed.friendships;
this.driversById = new Map(seed.drivers.map((d) => [d.id, d]));
}
async getFeedForDriver(driverId: string, limit?: number): Promise<FeedItem[]> {
const friendIds = new Set(
this.friendships
.filter((f) => f.driverId === driverId)
.map((f) => f.friendId),
);
this.logger.debug(`Getting feed for driver: ${driverId}, limit: ${limit}`);
try {
const friendIds = new Set(
this.friendships
.filter((f) => f.driverId === driverId)
.map((f) => f.friendId),
);
const items = this.feedEvents.filter((item) => {
if (item.actorDriverId && friendIds.has(item.actorDriverId)) {
return true;
}
return false;
});
const items = this.feedEvents.filter((item) => {
if (item.actorDriverId && friendIds.has(item.actorDriverId)) {
return true;
}
return false;
});
const sorted = items
.slice()
.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
const sorted = items
.slice()
.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
return typeof limit === 'number' ? sorted.slice(0, limit) : sorted;
this.logger.info(`Found ${sorted.length} feed items for driver: ${driverId}.`);
return typeof limit === 'number' ? sorted.slice(0, limit) : sorted;
} catch (error) {
this.logger.error(`Error getting feed for driver ${driverId}:`, error);
throw error;
}
}
async getGlobalFeed(limit?: number): Promise<FeedItem[]> {
const sorted = this.feedEvents
.slice()
.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
this.logger.debug(`Getting global feed, limit: ${limit}`);
try {
const sorted = this.feedEvents
.slice()
.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
return typeof limit === 'number' ? sorted.slice(0, limit) : sorted;
this.logger.info(`Found ${sorted.length} global feed items.`);
return typeof limit === 'number' ? sorted.slice(0, limit) : sorted;
} catch (error) {
this.logger.error(`Error getting global feed:`, error);
throw error;
}
}
}
export class InMemorySocialGraphRepository implements ISocialGraphRepository {
private readonly friendships: Friendship[];
private readonly driversById: Map<string, Driver>;
private readonly logger: ILogger;
constructor(seed: RacingSeedData) {
constructor(logger: ILogger, seed: RacingSeedData) {
this.logger = logger;
this.logger.info('InMemorySocialGraphRepository initialized.');
this.friendships = seed.friendships;
this.driversById = new Map(seed.drivers.map((d) => [d.id, d]));
}
async getFriendIds(driverId: string): Promise<string[]> {
return this.friendships
.filter((f) => f.driverId === driverId)
.map((f) => f.friendId);
this.logger.debug(`Getting friend IDs for driver: ${driverId}`);
try {
const friendIds = this.friendships
.filter((f) => f.driverId === driverId)
.map((f) => f.friendId);
this.logger.info(`Found ${friendIds.length} friend IDs for driver: ${driverId}.`);
return friendIds;
} catch (error) {
this.logger.error(`Error getting friend IDs for driver ${driverId}:`, error);
throw error;
}
}
async getFriends(driverId: string): Promise<Driver[]> {
const ids = await this.getFriendIds(driverId);
return ids
.map((id) => this.driversById.get(id))
.filter((d): d is Driver => Boolean(d));
this.logger.debug(`Getting friends for driver: ${driverId}`);
try {
const ids = await this.getFriendIds(driverId);
const friends = ids
.map((id) => this.driversById.get(id))
.filter((d): d is Driver => Boolean(d));
this.logger.info(`Found ${friends.length} friends for driver: ${driverId}.`);
return friends;
} catch (error) {
this.logger.error(`Error getting friends for driver ${driverId}:`, error);
throw error;
}
}
async getSuggestedFriends(driverId: string, limit?: number): Promise<Driver[]> {
const directFriendIds = new Set(await this.getFriendIds(driverId));
const suggestions = new Map<string, number>();
this.logger.debug(`Getting suggested friends for driver: ${driverId}, limit: ${limit}`);
try {
const directFriendIds = new Set(await this.getFriendIds(driverId));
const suggestions = new Map<string, number>();
for (const friendship of this.friendships) {
if (!directFriendIds.has(friendship.driverId)) continue;
const friendOfFriendId = friendship.friendId;
if (friendOfFriendId === driverId) continue;
if (directFriendIds.has(friendOfFriendId)) continue;
for (const friendship of this.friendships) {
if (!directFriendIds.has(friendship.driverId)) continue;
const friendOfFriendId = friendship.friendId;
if (friendOfFriendId === driverId) continue;
if (directFriendIds.has(friendOfFriendId)) continue;
suggestions.set(friendOfFriendId, (suggestions.get(friendOfFriendId) ?? 0) + 1);
suggestions.set(friendOfFriendId, (suggestions.get(friendOfFriendId) ?? 0) + 1);
}
const rankedIds = Array.from(suggestions.entries())
.sort((a, b) => b[1] - a[1])
.map(([id]) => id);
const drivers = rankedIds
.map((id) => this.driversById.get(id))
.filter((d): d is Driver => Boolean(d));
const result = typeof limit === 'number' ? drivers.slice(0, limit) : drivers;
this.logger.info(`Found ${result.length} suggested friends for driver: ${driverId}.`);
return result;
} catch (error) {
this.logger.error(`Error getting suggested friends for driver ${driverId}:`, error);
throw error;
}
const rankedIds = Array.from(suggestions.entries())
.sort((a, b) => b[1] - a[1])
.map(([id]) => id);
const drivers = rankedIds
.map((id) => this.driversById.get(id))
.filter((d): d is Driver => Boolean(d));
if (typeof limit === 'number') {
return drivers.slice(0, limit);
}
return drivers;
}
}