RedisVisitorTracker.cs 1.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263
  1. using Application.Abstractions.Cache;
  2. using Application.Abstractions.Chat;
  3. using Application.Abstractions.Hub;
  4. using StackExchange.Redis;
  5. using System.Text.Json;
  6. namespace Infrastructure.Hubs;
  7. /// <summary>
  8. /// AppHub 전역 접속자 추적 (Redis 기반).
  9. /// 단일 Hash `app:connections` 에 connectionId → ConnectedUser JSON 으로 기록 (게스트 포함).
  10. /// </summary>
  11. internal sealed class RedisVisitorTracker(IConnectionMultiplexer redis) : IVisitorTracker
  12. {
  13. private readonly IDatabase _db = redis.GetDatabase();
  14. private static readonly JsonSerializerOptions _jsonOptions = new()
  15. {
  16. PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
  17. WriteIndented = false
  18. };
  19. public async Task AddAsync(ConnectedUser user)
  20. {
  21. var json = JsonSerializer.Serialize(user, _jsonOptions);
  22. await _db.HashSetAsync(CacheKeys.AppConnections, user.ConnectionId, json);
  23. }
  24. public async Task RemoveAsync(string connectionId)
  25. {
  26. await _db.HashDeleteAsync(CacheKeys.AppConnections, connectionId);
  27. }
  28. public async Task<IReadOnlyList<ConnectedUser>> GetAllAsync()
  29. {
  30. var entries = await _db.HashGetAllAsync(CacheKeys.AppConnections);
  31. if (entries.Length <= 0)
  32. {
  33. return [];
  34. }
  35. var list = new List<ConnectedUser>(entries.Length);
  36. foreach (var entry in entries)
  37. {
  38. if (!entry.Value.IsNullOrEmpty)
  39. {
  40. var user = JsonSerializer.Deserialize<ConnectedUser>((string)entry.Value!, _jsonOptions);
  41. if (user is not null)
  42. {
  43. list.Add(user);
  44. }
  45. }
  46. }
  47. return list.AsReadOnly();
  48. }
  49. public async Task ClearAllAsync()
  50. {
  51. // 서버 시작 시 전체 초기화 (stale 제거) — 단일 키라 SCAN 불필요
  52. await _db.KeyDeleteAsync(CacheKeys.AppConnections);
  53. }
  54. }