namespace HrynCo.NotificationService.DAL.EF.Repositories; using HrynCo.NotificationService.DAL.Abstract.Push; using HrynCo.NotificationService.DAL.EF.Entities; using Microsoft.EntityFrameworkCore; public sealed class PushDeliveryRepository(NotificationDbContext db) : IPushDeliveryRepository { public async Task DeliverOnceAsync(Guid messageId, string serviceName, Func> deliver, CancellationToken cancellationToken) { // Thirty days exceeds the maximum payload lifetime; old replays cannot send expired data. var cutoff = DateTimeOffset.UtcNow.AddDays(-30); await db.Set().Where(x => x.CompletedAt < cutoff).ExecuteDeleteAsync(cancellationToken); await using var transaction = await db.Database.BeginTransactionAsync(cancellationToken); var identity = $"{serviceName}:{messageId:D}"; await db.Database.ExecuteSqlInterpolatedAsync( $"SELECT pg_advisory_xact_lock(hashtextextended({identity}, 1074))", cancellationToken); var previous = await db.Set().AsNoTracking() .SingleOrDefaultAsync(x => x.MessageId == messageId && x.ServiceName == serviceName, cancellationToken); if (previous is not null) return new PushDeliveryOutcome(previous.Error, previous.InvalidRegistration); // One bounded provider call while holding this message's lock prevents concurrent sends. // A crash after FCM accepts still requires client-side message-id deduplication. var outcome = await deliver(cancellationToken); db.Add(new PushDeliveryEntity { MessageId = messageId, ServiceName = serviceName, Error = outcome.Error, InvalidRegistration = outcome.InvalidRegistration, CompletedAt = DateTimeOffset.UtcNow }); await db.SaveChangesAsync(cancellationToken); await transaction.CommitAsync(cancellationToken); return outcome; } }