diff --git a/DysonNetwork.Pass/Account/AccountEventService.cs b/DysonNetwork.Pass/Account/AccountEventService.cs index ffe9f7f..1156761 100644 --- a/DysonNetwork.Pass/Account/AccountEventService.cs +++ b/DysonNetwork.Pass/Account/AccountEventService.cs @@ -478,6 +478,54 @@ public class AccountEventService( return activities; } + public async Task>> GetActiveActivitiesBatch(List userIds) + { + var results = new Dictionary>(); + var cacheMissUserIds = new List(); + + // Try to get activities from cache first + foreach (var userId in userIds) + { + var cacheKey = $"{ActivityCacheKey}{userId}"; + var cachedActivities = await cache.GetAsync>(cacheKey); + if (cachedActivities != null) + { + results[userId] = cachedActivities; + } + else + { + cacheMissUserIds.Add(userId); + } + } + + // If all activities were found in cache, return early + if (cacheMissUserIds.Count == 0) return results; + + // Fetch remaining activities from database in a single query + var now = SystemClock.Instance.GetCurrentInstant(); + var activitiesFromDb = await db.PresenceActivities + .Where(e => cacheMissUserIds.Contains(e.AccountId) && e.LeaseExpiresAt > now && e.DeletedAt == null) + .ToListAsync(); + + // Group activities by user ID and update cache + var activitiesByUser = activitiesFromDb + .GroupBy(a => a.AccountId) + .ToDictionary(g => g.Key, g => g.ToList()); + + foreach (var userId in cacheMissUserIds) + { + var userActivities = activitiesByUser.GetValueOrDefault(userId, new List()); + results[userId] = userActivities; + + // Update cache for this user + var cacheKey = $"{ActivityCacheKey}{userId}"; + await cache.SetWithGroupsAsync(cacheKey, userActivities, [$"{AccountService.AccountCachePrefix}{userId}"], + TimeSpan.FromMinutes(1)); + } + + return results; + } + public async Task<(List, int)> GetAllActivities(Guid userId, int offset = 0, int take = 20) { var query = db.PresenceActivities diff --git a/DysonNetwork.Pass/Account/FriendsController.cs b/DysonNetwork.Pass/Account/FriendsController.cs new file mode 100644 index 0000000..ff5f3f0 --- /dev/null +++ b/DysonNetwork.Pass/Account/FriendsController.cs @@ -0,0 +1,60 @@ +using DysonNetwork.Shared.Models; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace DysonNetwork.Pass.Account; + +[ApiController] +[Route("/api/friends")] +public class FriendsController(AppDatabase db, RelationshipService rels, AccountEventService events) : ControllerBase +{ + public class FriendOverviewItem + { + public SnAccount Account { get; set; } = null!; + public SnAccountStatus Status { get; set; } = null!; + public List Activities { get; set; } = []; + } + + [HttpGet("overview")] + [Authorize] + public async Task>> GetOverview() + { + if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); + + var friendIds = await rels.ListAccountFriends(currentUser); + + // Fetch data in parallel using batch methods for better performance + var accountsTask = db.Accounts + .Where(a => friendIds.Contains(a.Id)) + .Include(a => a.Profile) + .ToListAsync(); + + var statusesTask = events.GetStatuses(friendIds); + var activitiesTask = events.GetActiveActivitiesBatch(friendIds); + + // Wait for all data to be fetched + await Task.WhenAll(accountsTask, statusesTask, activitiesTask); + + var accounts = accountsTask.Result; + var statuses = statusesTask.Result; + var activities = activitiesTask.Result; + + var result = new List(); + + foreach (var account in accounts) + { + var status = statuses.GetValueOrDefault(account.Id); + var accountActivities = activities.GetValueOrDefault(account.Id, []); + + result.Add(new FriendOverviewItem + { + Account = account, + Status = status ?? new SnAccountStatus { AccountId = account.Id }, + Activities = accountActivities + }); + } + + return Ok(result); + } +}