This commit is contained in:
2025-12-04 11:54:42 +01:00
parent 9d5caa87f3
commit b7d5551ea7
223 changed files with 5473 additions and 885 deletions

View File

@@ -0,0 +1,8 @@
export interface CurrentUserSocialDTO {
driverId: string;
displayName: string;
avatarUrl: string;
countryCode: string;
primaryTeamId?: string;
primaryLeagueId?: string;
}

View File

@@ -0,0 +1,9 @@
export interface FriendDTO {
driverId: string;
displayName: string;
avatarUrl: string;
isOnline: boolean;
lastSeen: Date;
primaryLeagueId?: string;
primaryTeamId?: string;
}

View File

@@ -0,0 +1,17 @@
import type { FeedItemType } from '../value-objects/FeedItemType';
export interface FeedItem {
id: string;
timestamp: Date;
type: FeedItemType;
actorFriendId?: string;
actorDriverId?: string;
leagueId?: string;
raceId?: string;
teamId?: string;
position?: number;
headline: string;
body?: string;
ctaLabel?: string;
ctaHref?: string;
}

View File

@@ -0,0 +1,6 @@
import type { FeedItem } from '../entities/FeedItem';
export interface IFeedRepository {
getFeedForDriver(driverId: string, limit?: number): Promise<FeedItem[]>;
getGlobalFeed(limit?: number): Promise<FeedItem[]>;
}

View File

@@ -0,0 +1,7 @@
import type { Driver } from '@gridpilot/racing/domain/entities/Driver';
export interface ISocialGraphRepository {
getFriends(driverId: string): Promise<Driver[]>;
getFriendIds(driverId: string): Promise<string[]>;
getSuggestedFriends(driverId: string, limit?: number): Promise<Driver[]>;
}

View File

@@ -0,0 +1,8 @@
export type FeedItemType =
| 'friend-joined-league'
| 'friend-joined-team'
| 'friend-finished-race'
| 'friend-new-personal-best'
| 'new-race-scheduled'
| 'new-result-posted'
| 'league-highlight';

View File

@@ -0,0 +1,106 @@
import type { Driver } from '@gridpilot/racing/domain/entities/Driver';
import type { FeedItem } from '@gridpilot/social/domain/entities/FeedItem';
import type { IFeedRepository } from '@gridpilot/social/domain/repositories/IFeedRepository';
import type { ISocialGraphRepository } from '@gridpilot/social/domain/repositories/ISocialGraphRepository';
export type Friendship = {
driverId: string;
friendId: string;
};
export type RacingSeedData = {
drivers: Driver[];
friendships: Friendship[];
feedEvents: FeedItem[];
};
export class InMemoryFeedRepository implements IFeedRepository {
private readonly feedEvents: FeedItem[];
private readonly friendships: Friendship[];
private readonly driversById: Map<string, Driver>;
constructor(seed: RacingSeedData) {
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),
);
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());
return typeof limit === 'number' ? sorted.slice(0, limit) : sorted;
}
async getGlobalFeed(limit?: number): Promise<FeedItem[]> {
const sorted = this.feedEvents
.slice()
.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
return typeof limit === 'number' ? sorted.slice(0, limit) : sorted;
}
}
export class InMemorySocialGraphRepository implements ISocialGraphRepository {
private readonly friendships: Friendship[];
private readonly driversById: Map<string, Driver>;
constructor(seed: RacingSeedData) {
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);
}
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));
}
async getSuggestedFriends(driverId: string, limit?: number): Promise<Driver[]> {
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;
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));
if (typeof limit === 'number') {
return drivers.slice(0, limit);
}
return drivers;
}
}

View File

@@ -0,0 +1,10 @@
{
"name": "@gridpilot/social",
"version": "0.1.0",
"type": "module",
"exports": {
"./domain/*": "./domain/*",
"./application/*": "./application/*",
"./infrastructure/*": "./infrastructure/*"
}
}

View File

@@ -0,0 +1,11 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": ".",
"outDir": "dist",
"composite": false,
"declaration": true,
"declarationMap": false
},
"include": ["./**/*.ts"]
}