Инициализировать репозиторий «Дейл»

Первый коммит: модульный монолит ядра (.NET 10) и gRPC-сервисы
ai/ml/telegram, фронтенд Vue 3/Vite/Tailwind, документация (ТЗ,
инструкция пользователя, техдокументация, код-стайл), бэклог,
скрипты развёртывания и архив прототипа LeadRadar.
This commit is contained in:
Rustam Khalimov
2026-09-11 02:50:17 +03:00
commit 9e07568ddd
1402 changed files with 177470 additions and 0 deletions
@@ -0,0 +1,117 @@
using System.Diagnostics.CodeAnalysis;
namespace Deal.Telegram.Caching;
/// <summary>
/// Кэш с ограниченной ёмкостью и вытеснением least-recently-used (LRU): при переполнении удаляется
/// элемент, к которому дольше всего не обращались. Нужен там, где раньше жил неограниченный
/// <see cref="Dictionary{TKey,TValue}"/> и память росла с числом сущностей (долгоживущий процесс сервиса).
/// </summary>
/// <remarks>
/// НЕ потокобезопасен: вызывающий обязан сериализовать доступ (в telegram-service кэши
/// WTelegramSessionClient защищены общим <c>_entityCacheGate</c>). Вытеснение — забота производительности,
/// а не корректности: потерянное значение всегда можно получить повторным запросом к Telegram.
/// </remarks>
/// <typeparam name="TKey">Тип ключа (ссылочный или значимый).</typeparam>
/// <typeparam name="TValue">Тип значения.</typeparam>
public sealed class LruCache<TKey, TValue>
where TKey : notnull
{
private readonly Dictionary<TKey, LinkedListNode<Entry>> _map;
private readonly LinkedList<Entry> _recency = new();
/// <summary>
/// Создаёт кэш заданной ёмкости.
/// </summary>
/// <param name="capacity">Максимальное число элементов (должно быть больше нуля).</param>
/// <exception cref="ArgumentOutOfRangeException">Ёмкость не положительна.</exception>
public LruCache(int capacity)
{
if (capacity <= 0)
{
throw new ArgumentOutOfRangeException(nameof(capacity), capacity, "Ёмкость LRU-кэша должна быть положительной");
}
Capacity = capacity;
_map = new Dictionary<TKey, LinkedListNode<Entry>>(capacity);
}
/// <summary>
/// Максимальное число элементов кэша.
/// </summary>
public int Capacity { get; }
/// <summary>
/// Текущее число элементов кэша.
/// </summary>
public int Count => _map.Count;
/// <summary>
/// Пробует получить значение по ключу, отмечая его как недавно использованный.
/// </summary>
/// <param name="key">Ключ.</param>
/// <param name="value">Найденное значение (иначе значение по умолчанию).</param>
/// <returns>True — ключ найден.</returns>
public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value)
{
if (!_map.TryGetValue(key, out LinkedListNode<Entry>? node))
{
value = default;
return false;
}
Touch(node);
value = node.Value.Value;
return true;
}
/// <summary>
/// Проверяет наличие ключа, не меняя недавность (лёгкая проверка без вытеснения).
/// </summary>
/// <param name="key">Ключ.</param>
/// <returns>True — ключ есть в кэше.</returns>
public bool ContainsKey(TKey key) => _map.ContainsKey(key);
/// <summary>
/// Записывает значение по ключу (вставка или обновление) и вытесняет самый давний элемент при
/// переполнении. Обновление существующего ключа делает его самым недавним.
/// </summary>
/// <param name="key">Ключ.</param>
/// <param name="value">Значение.</param>
public void Set(TKey key, TValue value)
{
if (_map.TryGetValue(key, out LinkedListNode<Entry>? existing))
{
existing.Value = new Entry(key, value);
Touch(existing);
return;
}
var node = new LinkedListNode<Entry>(new Entry(key, value));
_recency.AddFirst(node);
_map[key] = node;
if (_map.Count > Capacity)
{
LinkedListNode<Entry> oldest = _recency.Last!;
_recency.RemoveLast();
_map.Remove(oldest.Value.Key);
}
}
// Перемещает узел в начало списка недавности (последний использованный).
// node: Узел кэша.
private void Touch(LinkedListNode<Entry> node)
{
if (!ReferenceEquals(node, _recency.First))
{
_recency.Remove(node);
_recency.AddFirst(node);
}
}
// Элемент кэша (ключ хранится для удаления при вытеснении из списка недавности).
// Key: Ключ.
// Value: Значение.
private readonly record struct Entry(TKey Key, TValue Value);
}