using System.Collections.ObjectModel; namespace Deal.Telegram.Dialogs; /// /// Зеркало каталога диалогов тенанта в памяти telegram-service (план Task 10, Dialogs/DialogCatalog.cs; /// Ruling 7: «сервис держит зеркало мониторинга в памяти», ядро — владелец списка мониторинга в своей БД). /// /// Тенант хранит два набора id диалогов: полный каталог (из refresh_dialogs/realtime_sweep, L468–519) /// и подмножество с включённым мониторингом. Мониторинг актуализируется тремя путями: /// * командой SetMonitor/SetMonitorAll ядра (обновление одной записи / всех сразу); /// * ответом Ingress.SyncDialogs (ядро применило entries с autoMonitorNew — L244–246 «_reload_monitored»); /// * очисткой после Logout/отключения аккаунта (прототип L203: `_monitored.clear()`). /// Все операции потокобезопасны; зеркало чисто в памяти — потерю при рестарте догоняет realtime_sweep. /// public sealed class DialogCatalog { private readonly object _gate = new(); private readonly Dictionary> _knownByTenant = new(StringComparer.Ordinal); private readonly Dictionary> _monitoredByTenant = new(StringComparer.Ordinal); /// /// Обновляет полный каталог диалогов тенанта (entries refresh/realtime_sweep). Мониторинг при этом /// не трогается — актуальный monitored-набор приходит ответом SyncDialogs (или командами SetMonitor). /// /// Id тенанта. /// Актуальные id каталога (entries списка диалогов). public void ReplaceKnown(string tenantId, IEnumerable dialogIds) { ArgumentException.ThrowIfNullOrWhiteSpace(tenantId); lock (_gate) { _knownByTenant[tenantId] = new HashSet(dialogIds, StringComparer.Ordinal); } } /// /// Включает/выключает мониторинг одного диалога (SetMonitor RPC, set_monitor L536–546). Команда /// приходит от ядра после обновления его БД; зеркало повторяет решение без собственной проверки /// «известности» — запись может появиться раньше каталога (включение после рестарта сервиса). /// /// Id тенанта. /// Id диалога. /// Мониторить (true) или выключить (false). public void SetMonitored( string tenantId, string dialogId, bool enabled) { ArgumentException.ThrowIfNullOrWhiteSpace(tenantId); ArgumentException.ThrowIfNullOrWhiteSpace(dialogId); lock (_gate) { HashSet monitored = MonitoredSet(tenantId); if (enabled) { monitored.Add(dialogId); } else { monitored.Remove(dialogId); } } } /// /// Заменяет monitored-набор ответом SyncDialogs (актуальный список ядра после SyncFromTelegram — /// авто-мониторинг новых/удаление отсутствующих, Ruling 7). Это авторитетный источник зеркала. /// /// Id тенанта. /// Id диалогов с включённым мониторингом. public void ReplaceMonitored(string tenantId, IEnumerable monitoredIds) { ArgumentException.ThrowIfNullOrWhiteSpace(tenantId); lock (_gate) { _monitoredByTenant[tenantId] = new HashSet(monitoredIds, StringComparer.Ordinal); } } /// /// Включает/выключает мониторинг всех диалогов каталога (SetMonitorAll RPC, set_monitor_all /// L548–567). При включении учитываются и уже мониторящиеся записи (каталог может отставать от БД /// ядра после рестарта — «монитор» из ответа SyncDialogs приедет следующим циклом realtime_sweep). /// /// Id тенанта. /// Мониторить все (true) или снять мониторинг со всех (false). /// Сколько диалогов в каталоге тенанта (SetMonitorAllReply.count). public int SetAllMonitored(string tenantId, bool enabled) { ArgumentException.ThrowIfNullOrWhiteSpace(tenantId); lock (_gate) { HashSet known = KnownSet(tenantId); if (enabled) { HashSet monitored = MonitoredSet(tenantId); monitored.UnionWith(known); } else { _monitoredByTenant[tenantId] = new HashSet(StringComparer.Ordinal); } return known.Count; } } /// /// Проверяет, мониторится ли диалог (фильтр realtime-событий L262 и догона sweep L429). /// /// Id тенанта. /// Id диалога. /// True — диалог в monitored-наборе тенанта. public bool IsMonitored(string tenantId, string dialogId) { if (string.IsNullOrWhiteSpace(tenantId) || string.IsNullOrWhiteSpace(dialogId)) { return false; } lock (_gate) { return _monitoredByTenant.TryGetValue(tenantId, out HashSet? monitored) && monitored.Contains(dialogId); } } /// /// Сколько диалогов знает каталог тенанта (ответ monitor-all, count каталога). /// /// Id тенанта. /// Размер каталога; 0 — каталог ещё не синхронизирован (нет refresh/sweep). public int KnownCount(string tenantId) { if (string.IsNullOrWhiteSpace(tenantId)) { return 0; } lock (_gate) { return _knownByTenant.TryGetValue(tenantId, out HashSet? known) ? known.Count : 0; } } /// /// Возвращает копию мониторящихся id тенанта (для логов/тестов). /// /// Id тенанта. /// Набор мониторящихся id (пустой, если тенанта нет). public IReadOnlyCollection SnapshotMonitored(string tenantId) { if (string.IsNullOrWhiteSpace(tenantId)) { return Array.Empty(); } lock (_gate) { return _monitoredByTenant.TryGetValue(tenantId, out HashSet? monitored) ? new ReadOnlyCollection(monitored.ToArray()) : Array.Empty(); } } /// /// Сбрасывает состояние тенанта (Logout/отключение аккаунта — прототип L203). /// /// Id тенанта. public void Reset(string tenantId) { if (string.IsNullOrWhiteSpace(tenantId)) { return; } lock (_gate) { _knownByTenant.Remove(tenantId); _monitoredByTenant.Remove(tenantId); } } // Полный каталог тенанта (создаёт пустой при первом обращении). Вызывается под _gate. // tenantId: Id тенанта. private HashSet KnownSet(string tenantId) { if (!_knownByTenant.TryGetValue(tenantId, out HashSet? known)) { known = new HashSet(StringComparer.Ordinal); _knownByTenant[tenantId] = known; } return known; } // Monitored-набор тенанта (создаёт пустой при первом обращении). Вызывается под _gate. // tenantId: Id тенанта. private HashSet MonitoredSet(string tenantId) { if (!_monitoredByTenant.TryGetValue(tenantId, out HashSet? monitored)) { monitored = new HashSet(StringComparer.Ordinal); _monitoredByTenant[tenantId] = monitored; } return monitored; } }