feat(missions): /load/index materializes viewer mission/achievement state

EnsureCurrentAsync now takes viewerId (was Viewer), so it works with
LoadController's AsNoTracking-loaded detached viewers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gamer147
2026-05-27 10:45:31 -04:00
parent 640a77ec6c
commit 5693ec0302
6 changed files with 27 additions and 18 deletions

View File

@@ -25,15 +25,13 @@ public sealed class ViewerMissionStateService : IViewerMissionStateService
_time = time;
}
public async Task EnsureCurrentAsync(Viewer viewer, CancellationToken ct = default)
public async Task EnsureCurrentAsync(long viewerId, CancellationToken ct = default)
{
// Always read fresh from DB. Navigation properties on the passed viewer may be stale or
// double-tracked depending on caller's load path — don't trust them.
var existingAchievements = await _viewerRepo.GetAchievementsAsync(viewer.Id, ct);
var existingMissions = await _viewerRepo.GetMissionsAsync(viewer.Id, ct);
var existingAchievements = await _viewerRepo.GetAchievementsAsync(viewerId, ct);
var existingMissions = await _viewerRepo.GetMissionsAsync(viewerId, ct);
await MaterializeAchievementsAsync(viewer.Id, existingAchievements, ct);
await EnsureMissionSlotsAsync(viewer.Id, existingMissions, ct);
await MaterializeAchievementsAsync(viewerId, existingAchievements, ct);
await EnsureMissionSlotsAsync(viewerId, existingMissions, ct);
}
private async Task MaterializeAchievementsAsync(long viewerId, List<ViewerAchievement> existing, CancellationToken ct)