Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning from v1.0.0 onwards. Prior 0.x releases may include breaking changes between minor versions.
Unreleased
[1.0.0] - 2026-05-22
Added
- Ractor compatibility audit —
spec/ractor_spec.rbdocuments the specific failure modes (non-shareable closures indefine_methodblocks,Ractor::IsolationErroronSafeMemoize.configuration); README section explains the limitation and the Thread-based workaround - Semantic versioning guarantee — README
## Public API and versioning guaranteesection enumerates every public constant, method, option key, andConfigurationattribute covered by semver from v1.0.0 onwards; opt-in extensions (SafeMemoize::Rails,SafeMemoize::Adapters::*) are explicitly called out as not yet covered until their owning milestone ships - Full API reference — YARD documentation added to all public methods, classes, and modules;
SafeMemoize::Adapters::StatsDandSafeMemoize::Adapters::OpenTelemetryfully documented with usage examples; internal modules marked@api private;.yardoptsandrake doctask added;gem "yard"added as a development dependency - Deprecation sweep — pre-v1.0.0 API consistency audit:
memoized?,memo_ttl_remaining,memo_touch,memo_age,memo_stale?now usecompute_cache_keyinstead ofsafe_memo_cache_keyso they correctly resolve entries stored with a custom key (instance-levelmemoize_with_custom_keyor class-levelkey:);memo_matcher_for(used byreset_memoandmemo_refresh) receives the same fix;SafeMemoize::Erroradded to the public API guarantee table and to RBS + Sorbet signatures; RBS and.rbiwarm_memoblock annotation corrected back to mandatory (was incorrectly marked optional in v0.9.0 signatures) - Ruby version policy — README
## Ruby version supportsection formalises the supported version window (Ruby ≥ 3.3; current stable plus two previous non-EOL minors), the cadence for dropping EOL versions (minor release only, never a patch), and a history table of dropped versions; CI matrix documents covered versions with their EOL dates - Complete RBS + Sorbet signatures —
sig/safe_memoize.rbscorrected:SafeMemoize::Adapters::StatsDadded;memo_count,memo_keys,memo_valuesfixed from rest-arg to proper optional single arg;clear_memo_hooksandclear_custom_keysoptional-arg annotations corrected;warm_memoblock marked optional; newrbi/safe_memoize.rbiships Sorbet stubs covering the full public API, allConfigurationattributes, adapters, and opt-in Rails helpers - Upgrade guide —
UPGRADING.mddocuments every breaking change introduced across the 0.x series, with before/after code examples and migration steps for each; covers Ruby 3.2 removal, TTL clock change,memo_keys/memo_valuesshape change,memoizedefinition-time raise, argument mutation fix, hook exception isolation, and the two custom-key introspection fixes landing in v1.0.0
[0.9.0] - 2026-05-22
Added
ActiveSupport::Notificationsintegration — opt-in viaSafeMemoize.configure { |c| c.active_support_notifications = true }; emitscache_hit.safe_memoize,cache_miss.safe_memoize,cache_evict.safe_memoize,cache_expire.safe_memoize, andcache_store.safe_memoizeevents; each payload includes:method,:key, and:class; zero overhead when ActiveSupport is not loadedSafeMemoize::Adapters::StatsD— thin optional adapter that routes lifecycle events to any StatsD client viaSafeMemoize.configure { |c| c.statsd_client = my_client }; emitssafe_memoize.hit,safe_memoize.miss,safe_memoize.evict,safe_memoize.expire, andsafe_memoize.storewithmethod:andclass:tags; client errors are rescued and warned rather than raised- Formal benchmark suite (
benchmarks/benchmark.rb) — six scenarios covering zero-arg cache hit/miss, with-argument hit, fast vs locked path, shared vs instance cache, and concurrent throughput under 8-thread contention; optional comparisons againstmemeryandmemo_wise; run withbundle exec ruby benchmarks/benchmark.rb - Concurrency stress test suite (
spec/concurrency_spec.rb) — 18 barrier-synchronized examples hammering the fast path, locked path, and shared cache under 30 concurrent threads; covers exactly-once computation, LRU size invariant, hook count integrity, metric accuracy, TTL pruning, and deadlock detection (10-second timeout per run) SafeMemoize::Adapters::OpenTelemetry— optional adapter that wraps each cache-miss computation in an OpenTelemetry span; configure viaSafeMemoize.configure { |c| c.opentelemetry_tracer = OpenTelemetry.tracer_provider.tracer("safe_memoize") }; span name is"safe_memoize.compute"with attributessafe_memoize.method,safe_memoize.class, andsafe_memoize.cache_hit; falls back to untraced execution when the tracer is absent or does not respond toin_spanSafeMemoize::Rails— opt-in request-scope helpers (require "safe_memoize/rails"):SafeMemoize::Rails::RequestScopedconcern auto-registersafter_action :reset_all_memosin controllers and exposesreset_request_memoselsewhere;SafeMemoize::Rails::MiddlewareRack middleware resets all thread-tracked instances (SafeMemoize::Rails.track(self)) at the end of each request even on error
[0.8.0] - 2026-05-21
Added
- Raise
ArgumentErrorat definition time whenmemoizeis called on a method that does not exist on the class — previously the error only surfaced at runtime whensuperhad nothing to call - Key serialization safety: argument arrays, hashes, and strings are deep-frozen into an independent copy when the cache key is built, so callers that mutate their arguments after a call can no longer corrupt or miss the cached entry
memo_inspect— single-entry deep-inspection helper returning all metadata for one cached call in one mutex-held read:cached,value,hits,misses,ttl_remaining,age,custom_key, andlru_position; returnsnilwhen the entry is not cached- Deprecation infrastructure:
SafeMemoize.deprecate(subject, message:, horizon:)emits a structured[SafeMemoize]warning to stderr by default; configurable viaSafeMemoize.configure { |c| c.on_deprecation = ->(msg) { ... } }to raise, log, or collect warnings memoize_all only:— symmetric counterpart toexcept:; explicitly lists the methods to memoize and skips all others; raisesArgumentErrorwhen bothonly:andexcept:are given- Hook error isolation: exceptions raised inside lifecycle hooks no longer propagate to the caller; by default a
[SafeMemoize] Hook error in <type>: <message>warning is emitted to stderr; configurable viaSafeMemoize.configure { |c| c.on_hook_error = ->(error, hook_type, cache_key) { ... } }to raise, log, or silence
0.7.0 - 2026-05-18
Added
memo_preloadto batch-warm multiple cache entries in one call —obj.memo_preload(:find, [1], [2], [3])calls the memoized method for each arg set, caches all results, and returns them in input orderon_memo_storehook that fires whenever a value is written to the cache (miss,warm_memo, orload_memo); completes the full lifecycle hook set alongsideon_hit,on_miss,on_expire, andon_evictSafeMemoize.configurefor global default options —default_ttlanddefault_max_sizeapply to all subsequently memoized methods; per-call options override the global defaultsSafeMemoize.reset_configuration!to restore all global defaults tonilmemo_touchto reset the expiry clock on a cached entry without recomputing — accepts an optionalttl:override; returnstrueon success,falseif the entry is not cached or already expiredshared_memo_ageclass method to inspect how long ago a shared entry was cachedshared_memo_stale?class method to check whether a shared entry's TTL has elapsedkey:option onmemoizefor class-level cache key generation — calls whose key block returns the same value share one cache entry; instance-levelmemoize_with_custom_keystill takes prioritymemo_refreshto force-recompute a cached entry and store the new value in one callmemo_ageto return how many seconds ago an entry was cached (nilif not cached or expired)memo_stale?to check whether a cached entry exists but its TTL has elapsed
Changed
cache_metrics_resetnow accepts an optional method name to clear stats for a single method only; calling without arguments still clears all metricsshared:support inmemoize_allis now tested and documented (was already functional via**optionspassthrough)- RBS type signatures updated for all new methods and the
Configurationclass
0.6.3 - 2026-05-18
Changed
- Upgrade
softprops/action-gh-releasefrom v2 to v3 to resolve Node.js 20 deprecation warning in release workflow
0.6.2 - 2026-05-18
Added
- 100% line coverage across all lib files — added tests for edge cases in
CacheRecordMethods,CacheStoreMethods,InspectionMethods, andReleaseTooling; added SimpleCov filter to exclude/specfrom coverage reporting
0.6.1 - 2026-05-17
Changed
- Refactored
cache_stats/cache_stats_forto share aggregation logic via private helpers
Fixed
memo_keysandmemo_valuesshowedargs: custom_key, kwargs: nilfor methods usingmemoize_with_custom_key— now correctly surfaces ascustom_key:
0.6.0 - 2026-05-17
Added
ttl:option onwarm_memoso warmed entries can be given an expirymax_size:support forshared: truememoization (class-level LRU eviction)ttl_refresh: trueoption onmemoizefor sliding window TTL — resets the expiry clock on every cache hit so the entry only expires after a full TTL of inactivityinclude_protected:andinclude_private:options onmemoize_allmemo_ttl_remainingfor TTL introspection — returns seconds until expiry,nilfor no TTL,0for uncached or expired
Fixed
- TTL clock started at
memoizedefinition time instead of at first method call - Metrics key silently dropped kwargs, causing methods that differ only in kwargs to share a metrics bucket
- Stale LRU references remained in the order list after expired entries were pruned
0.5.0 - 2026-05-17
Removed
- Support for Ruby 3.2 (EOL); minimum required version is now Ruby 3.3
0.4.0 - 2026-05-17
Added
warm_memo,dump_memo, andload_memofor cache warm-up and persistence — pre-populate entries without calling the method, export live entries as a plain hash, and restore from a snapshotshared: trueoption onmemoizeto store results on the class instead of per-instance — includesreset_shared_memo,reset_all_shared_memos,shared_memoized?, andshared_memo_count; supportsttl:,if:, andunless:memoize_allto memoize every public method defined on the class in one call — accepts allmemoizeoptions plusexcept:to skip specific methodson_memo_misshook that fires on every cache miss, completing the full lifecycle hook set
0.3.0 - 2026-05-15
Added
on_memo_hithook that fires on every cache hit- Conditional memoization via
if:andunless:predicates onmemoize— uncached calls recompute on every invocation until the condition is satisfied; composes withttl:,max_size:, and hooks - LRU cache size limit via
max_size:onmemoize— evicts the least-recently-used entry when the limit is reached; cache hits promote entries; fireson_evict; thread-safe
0.2.0 - 2026-05-14
Added
- Optional TTL expiration for memoized entries
on_memo_expireandon_memo_evictlifecycle hooks;clear_memo_hooksto remove registered hooks- Cache metrics:
cache_stats,cache_stats_for,cache_hit_rate,cache_miss_rate, andcache_metrics_reset - Custom cache key generation via
memoize_with_custom_keyandclear_custom_keys
0.1.2 - 2026-05-13
Added
- Method visibility preservation (public, protected, private) for memoized methods
- Targeted
reset_memo— clear one cached argument combination or all entries for a method memoized?helper to check whether a specific call is cachedmemo_count,memo_keys, andmemo_valueshelpers for cache introspection
0.1.1 - 2026-05-13
Added
- Automated release tooling (
bin/release) and GitHub Actions workflow for RubyGems publishing and GitHub releases
0.1.0 - 2026-02-26
Added
- Initial release