Low commit activity in last 3 years
A thread-safe hash map where each key has its own TTL, with automatic expiration, max size eviction, expiration callbacks, and Enumerable support.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies
 Project Readme

philiprehberger-expiring_map

Tests Gem Version Last updated

Thread-safe hash with per-key TTL and automatic expiration

Requirements

  • Ruby >= 3.1

Installation

Add to your Gemfile:

gem "philiprehberger-expiring_map"

Or install directly:

gem install philiprehberger-expiring_map

Usage

require "philiprehberger/expiring_map"

cache = Philiprehberger::ExpiringMap.new(default_ttl: 300)
cache.set(:session, 'abc123')
cache.get(:session)  # => 'abc123'

Per-Key TTL

cache.set(:token, 'xyz', ttl: 60)    # expires in 60 seconds
cache.set(:config, data, ttl: 3600)   # expires in 1 hour
cache.ttl(:token)                      # => remaining seconds

Fetch-or-compute

cache = Philiprehberger::ExpiringMap.new(default_ttl: 300)

# On miss, the block is evaluated, the result stored, and returned.
# On hit, the stored value is returned and the block is not called.
user = cache.fetch(:user_42) { User.find(42) }

# Override the TTL for this insert only:
token = cache.fetch(:token, ttl: 60) { Auth.issue_token }

Max Size with Eviction

cache = Philiprehberger::ExpiringMap.new(default_ttl: 300, max_size: 1000)
# Oldest entries are evicted when capacity is reached

Expiration Callback

cache.on_expire do |key, value|
  logger.info("Expired: #{key}")
end

Touch to Reset TTL

cache.set(:session, data)
cache.touch(:session)  # resets TTL to default

Bulk Operations

cache.set_many({ user: "alice", role: "admin", theme: "dark" })
cache.set_many({ token: "abc", nonce: "xyz" }, ttl: 60)

result = cache.get_many(:user, :role, :missing)
# => { user: "alice", role: "admin", missing: nil }

Statistics

cache.set(:a, 1)
cache.get(:a)        # hit
cache.get(:missing)  # miss
cache.stats
# => { hits: 1, misses: 1, expirations: 0, evictions: 0, size: 1 }

Keys and Values

cache.set(:x, 10)
cache.set(:y, 20)
cache.keys    # => [:x, :y]
cache.values  # => [10, 20]

Predicate Deletion

cache.set(:a, 1)
cache.set(:b, 10)
cache.delete_if { |_key, value| value >= 10 }  # => 1

Enumerable

cache.each { |key, value| puts "#{key}: #{value}" }
cache.select { |_k, v| v > 10 }

Manual Sweep

Most read methods (get, size, each, keys, values) sweep expired entries lazily on access. Long-running processes that read sparingly can accumulate stale entries between reads — call purge_expired! to reclaim memory and fire on_expire callbacks for everything that has expired:

cache.set(:a, 1, ttl: 0.01)
cache.set(:b, 2, ttl: 10)
sleep 0.02

cache.purge_expired!  # => 1   (number of entries removed)
cache.keys             # => [:b]

expired?(key) returns whether a present key has elapsed its TTL — without deleting the entry or firing on_expire. Distinct from get(key).nil?, which can't tell "missing" from "expired":

cache.set(:dead, 'gone', ttl: 0.01)
sleep 0.02
cache.expired?(:dead)   # => true
cache.expired?(:nope)   # => false (missing, not expired)

API

Method Description
.new(default_ttl:, max_size:) Create a new expiring map
#set(key, value, ttl:) Store a value with optional per-key TTL
#get(key) Retrieve a value, nil if expired or missing
#fetch(key, ttl:) { block } Return stored value or atomically memoize block result on miss
#set_many(hash, ttl:) Bulk insert from a hash
#get_many(*keys) Bulk get returning hash of key => value
#delete(key) Remove and return a value
#delete_if { |k, v| } Remove entries where block returns true
#ttl(key) Return remaining TTL in seconds
#touch(key) Reset TTL to default
#size Count of non-expired entries
#keys Array of non-expired keys
#values Array of non-expired values
#stats Hash of hits, misses, expirations, evictions, size
#on_expire { |k, v| } Register expiration callback
#clear Remove all entries
#each { |k, v| } Iterate over non-expired entries
#purge_expired! Actively sweep expired entries; fires on_expire; returns the count removed
#expired?(key) Whether the entry at key is present-but-expired (does not delete or fire on_expire)

Development

bundle install
bundle exec rspec
bundle exec rubocop

Support

If you find this project useful:

Star the repo

🐛 Report issues

💡 Suggest features

❤️ Sponsor development

🌐 All Open Source Projects

💻 GitHub Profile

🔗 LinkedIn Profile

License

MIT