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\
#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\
#clear Remove all entries
`#each { \ k, v\
#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