#pragma once #include #include #include #include #include namespace ling3 { // Caller serializes access. Values should own immutable snapshots, so an active // reader can safely retain a copy even after the cache releases its reference. template class StateCache { struct Entry { std::string key; T value; std::size_t bytes; }; using Iterator = typename std::list::iterator; std::list entries_; std::unordered_map index_; std::size_t limit_, bytes_ = 0, evictions_ = 0; public: explicit StateCache(std::size_t limit) : limit_(limit) {} StateCache(const StateCache &) = delete; StateCache & operator=(const StateCache &) = delete; StateCache(StateCache &&) = default; StateCache & operator=(StateCache &&) = default; const T * Get(const std::string & key) { auto found = index_.find(key); if (found == index_.end()) return nullptr; entries_.splice(entries_.begin(), entries_, found->second); return &found->second->value; } bool Erase(const std::string & key) { auto found = index_.find(key); if (found == index_.end()) return false; bytes_ -= found->second->bytes; entries_.erase(found->second); index_.erase(found); return true; } bool Put(std::string key, T value, std::size_t bytes) { // An oversized update must not leave an old version under the same key. Erase(key); if (!limit_ || bytes > limit_) return false; while (!entries_.empty() && bytes_ > limit_-bytes) { Erase(entries_.back().key); ++evictions_; } entries_.push_front({std::move(key), std::move(value), bytes}); try { index_.emplace(entries_.front().key, entries_.begin()); } catch (...) { entries_.pop_front(); throw; } bytes_ += bytes; return true; } void Clear() { index_.clear(); entries_.clear(); bytes_ = 0; } std::size_t bytes() const { return bytes_; } std::size_t limit() const { return limit_; } std::size_t size() const { return entries_.size(); } std::size_t evictions() const { return evictions_; } }; } // namespace ling3