| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263 |
- using Application.Abstractions.Cache;
- using Application.Abstractions.Chat;
- using Application.Abstractions.Hub;
- using StackExchange.Redis;
- using System.Text.Json;
- namespace Infrastructure.Hubs;
- /// <summary>
- /// AppHub 전역 접속자 추적 (Redis 기반).
- /// 단일 Hash `app:connections` 에 connectionId → ConnectedUser JSON 으로 기록 (게스트 포함).
- /// </summary>
- internal sealed class RedisVisitorTracker(IConnectionMultiplexer redis) : IVisitorTracker
- {
- private readonly IDatabase _db = redis.GetDatabase();
- private static readonly JsonSerializerOptions _jsonOptions = new()
- {
- PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
- WriteIndented = false
- };
- public async Task AddAsync(ConnectedUser user)
- {
- var json = JsonSerializer.Serialize(user, _jsonOptions);
- await _db.HashSetAsync(CacheKeys.AppConnections, user.ConnectionId, json);
- }
- public async Task RemoveAsync(string connectionId)
- {
- await _db.HashDeleteAsync(CacheKeys.AppConnections, connectionId);
- }
- public async Task<IReadOnlyList<ConnectedUser>> GetAllAsync()
- {
- var entries = await _db.HashGetAllAsync(CacheKeys.AppConnections);
- if (entries.Length <= 0)
- {
- return [];
- }
- var list = new List<ConnectedUser>(entries.Length);
- foreach (var entry in entries)
- {
- if (!entry.Value.IsNullOrEmpty)
- {
- var user = JsonSerializer.Deserialize<ConnectedUser>((string)entry.Value!, _jsonOptions);
- if (user is not null)
- {
- list.Add(user);
- }
- }
- }
- return list.AsReadOnly();
- }
- public async Task ClearAllAsync()
- {
- // 서버 시작 시 전체 초기화 (stale 제거) — 단일 키라 SCAN 불필요
- await _db.KeyDeleteAsync(CacheKeys.AppConnections);
- }
- }
|