feat(replay): add ReplayHistoryReader for newest-first list query
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
7
SVSim.Database/Services/Replay/IReplayHistoryReader.cs
Normal file
7
SVSim.Database/Services/Replay/IReplayHistoryReader.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace SVSim.Database.Services.Replay;
|
||||||
|
|
||||||
|
public interface IReplayHistoryReader
|
||||||
|
{
|
||||||
|
/// <summary>Newest-first by CreateTime. Caps at <paramref name="take"/> (default 50).</summary>
|
||||||
|
Task<IReadOnlyList<ReplayHistoryEntry>> GetRecentAsync(long viewerId, int take, CancellationToken ct);
|
||||||
|
}
|
||||||
27
SVSim.Database/Services/Replay/ReplayHistoryEntry.cs
Normal file
27
SVSim.Database/Services/Replay/ReplayHistoryEntry.cs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
namespace SVSim.Database.Services.Replay;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Read-side row returned by <see cref="IReplayHistoryReader"/>. The /replay/info
|
||||||
|
/// controller maps this to its wire DTO (all-stringified per prod capture).
|
||||||
|
/// </summary>
|
||||||
|
public sealed record ReplayHistoryEntry(
|
||||||
|
long BattleId,
|
||||||
|
int BattleType,
|
||||||
|
int DeckFormat,
|
||||||
|
int TwoPickType,
|
||||||
|
int IsLimitTurn,
|
||||||
|
int SelfClassId,
|
||||||
|
int SelfSubClassId,
|
||||||
|
int SelfCharaId,
|
||||||
|
string SelfRotationId,
|
||||||
|
int OpponentClassId,
|
||||||
|
int OpponentSubClassId,
|
||||||
|
int OpponentCharaId,
|
||||||
|
string OpponentName,
|
||||||
|
string OpponentCountryCode,
|
||||||
|
long OpponentEmblemId,
|
||||||
|
long OpponentDegreeId,
|
||||||
|
string OpponentRotationId,
|
||||||
|
bool IsWin,
|
||||||
|
DateTime BattleStartTime,
|
||||||
|
DateTime CreateTime);
|
||||||
27
SVSim.Database/Services/Replay/ReplayHistoryReader.cs
Normal file
27
SVSim.Database/Services/Replay/ReplayHistoryReader.cs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace SVSim.Database.Services.Replay;
|
||||||
|
|
||||||
|
public sealed class ReplayHistoryReader : IReplayHistoryReader
|
||||||
|
{
|
||||||
|
private readonly SVSimDbContext _db;
|
||||||
|
|
||||||
|
public ReplayHistoryReader(SVSimDbContext db) => _db = db;
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<ReplayHistoryEntry>> GetRecentAsync(long viewerId, int take, CancellationToken ct)
|
||||||
|
{
|
||||||
|
return await _db.ViewerBattleHistories
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(h => h.ViewerId == viewerId)
|
||||||
|
.OrderByDescending(h => h.CreateTime)
|
||||||
|
.Take(take)
|
||||||
|
.Select(h => new ReplayHistoryEntry(
|
||||||
|
h.BattleId, h.BattleType, h.DeckFormat, h.TwoPickType, h.IsLimitTurn,
|
||||||
|
h.SelfClassId, h.SelfSubClassId, h.SelfCharaId, h.SelfRotationId,
|
||||||
|
h.OpponentClassId, h.OpponentSubClassId, h.OpponentCharaId,
|
||||||
|
h.OpponentName, h.OpponentCountryCode,
|
||||||
|
h.OpponentEmblemId, h.OpponentDegreeId, h.OpponentRotationId,
|
||||||
|
h.IsWin, h.BattleStartTime, h.CreateTime))
|
||||||
|
.ToListAsync(ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -119,6 +119,7 @@ public class Program
|
|||||||
|
|
||||||
builder.Services.AddSingleton<IBattleContextStore, InMemoryBattleContextStore>();
|
builder.Services.AddSingleton<IBattleContextStore, InMemoryBattleContextStore>();
|
||||||
builder.Services.AddScoped<IBattleHistoryWriter, BattleHistoryWriter>();
|
builder.Services.AddScoped<IBattleHistoryWriter, BattleHistoryWriter>();
|
||||||
|
builder.Services.AddScoped<IReplayHistoryReader, ReplayHistoryReader>();
|
||||||
|
|
||||||
// Deck-code mint/resolve uses IMemoryCache for ephemeral (3-min TTL) storage; no DB
|
// Deck-code mint/resolve uses IMemoryCache for ephemeral (3-min TTL) storage; no DB
|
||||||
// row, no migration. Singleton because the cache + RNG seam are process-wide.
|
// row, no migration. Singleton because the cache + RNG seam are process-wide.
|
||||||
|
|||||||
Reference in New Issue
Block a user