using DeepDrftData.Data; using DeepDrftModels.Entities; using DeepDrftModels.Enums; using Microsoft.EntityFrameworkCore; namespace DeepDrftData.Repositories; /// /// Data access for the Phase 16 anonymous telemetry tables (all SQL — the FileDatabase vault is not /// involved). Owns the append-only writes to play_event / share_event and the /// incremental-on-write bump of the play_counter rollup (D6). Server-side release resolution /// (§2.3 / D4) lives here: a play event carries only the track key, and this repository joins /// track→release at write time and stamps the release id on the row. /// /// /// Unlike these entities are not BaseEntity/IEntity (no /// soft-delete lifecycle), so this is a plain context-backed repository rather than an extension of the /// BlazorBlocks Repository<> base. It holds the same scoped /// the rest of the SQL layer uses, never a service locator. /// /// public class EventRepository { private readonly DeepDrftContext _context; public EventRepository(DeepDrftContext context) { _context = context; } /// /// Append one play event and bump the track's counter in a single transaction (D6). The release id /// is resolved here from the track key (§2.3 / D4): a live track contributes its release id (null /// for a loose track); an unknown key records the event with a null release and no counter bump /// (there is no track to roll up against). Returns true when the event was written. /// public async Task RecordPlayAsync( string trackEntryKey, PlayBucket bucket, string? anonId, CancellationToken ct = default) { // Resolve the track→release link server-side. Soft-deleted tracks resolve to null so a play of // a since-removed track still logs (with no counter bump) rather than throwing. var track = await _context.Tracks .Where(t => t.EntryKey == trackEntryKey && !t.IsDeleted) .Select(t => new { t.Id, t.ReleaseId }) .FirstOrDefaultAsync(ct); // The append and the counter bump must commit together — wrap them in one transaction so a // counter that drifts from the log is impossible. Reuse an ambient transaction if the caller // already opened one. var ownsTransaction = _context.Database.CurrentTransaction is null; var transaction = ownsTransaction ? await _context.Database.BeginTransactionAsync(ct) : null; try { _context.PlayEvents.Add(new PlayEvent { TrackEntryKey = trackEntryKey, ReleaseId = track?.ReleaseId, Bucket = bucket, AnonId = anonId, CreatedAt = DateTime.UtcNow, }); if (track is not null) await BumpCounterAsync(track.Id, bucket, ct); await _context.SaveChangesAsync(ct); if (transaction is not null) await transaction.CommitAsync(ct); return true; } catch { if (transaction is not null) await transaction.RollbackAsync(ct); throw; } finally { if (transaction is not null) await transaction.DisposeAsync(); } } /// Append one share event. No rollup table for shares in wave 16.1 — a plain insert. public async Task RecordShareAsync( ShareTargetType targetType, string targetKey, ShareChannel channel, string? anonId, CancellationToken ct = default) { _context.ShareEvents.Add(new ShareEvent { TargetType = targetType, TargetKey = targetKey, Channel = channel, AnonId = anonId, CreatedAt = DateTime.UtcNow, }); await _context.SaveChangesAsync(ct); } // Bump the matching bucket column on the track's counter row, creating the row on first play. The // row is added to the change tracker but not saved here — the caller's SaveChanges/commit persists // it inside the same transaction as the event append. private async Task BumpCounterAsync(long trackId, PlayBucket bucket, CancellationToken ct) { var counter = await _context.PlayCounters.FirstOrDefaultAsync(c => c.TrackId == trackId, ct); if (counter is null) { counter = new PlayCounter { TrackId = trackId }; _context.PlayCounters.Add(counter); } switch (bucket) { case PlayBucket.Partial: counter.PartialCount++; break; case PlayBucket.Sampled: counter.SampledCount++; break; case PlayBucket.Complete: counter.CompleteCount++; break; } } }