safe_memoize 0.6.3 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fcdb1584d3b3306021fa9e49007f686f561cd2658ea811e97fc18d89e36cb20e
4
- data.tar.gz: bc69db3f6d77baf031c6a1b92004ca2b4d5c60346b9546ec29388231e2d3d782
3
+ metadata.gz: 62f1dae5d57d99d59fe20d981bca17d04bd1ff5b9dd09175770a51bf3d49dd03
4
+ data.tar.gz: fade31de5124ed4b4f5d88f8bd62aaf750ec03cb22bdf97889fed2283ad3a58d
5
5
  SHA512:
6
- metadata.gz: f1a8bb8cd2ee519a39b90ed96ebac7a700d759e05bc0561225663ee24b6f595660f25da36dda2db4509fb521a9048a38b904760a06e2d09c2347fd52f5011db0
7
- data.tar.gz: dc1d3b24ecc9fe4a747f1ba12f2a9af335d61f671f1261203a43ff831fc7460dce8e1adf2142c771ff93d5f278127f9a9a54fe408b4361a8a585cebce8ba65a0
6
+ metadata.gz: e742795adaef41bc07e32d1ffb0a1f78335eae46daca57dca3b6b27cf4fc1eb6a58e1636e05d21814336ab3e54468b6ffbe0a4ea52f017c00b0df1a70b98eaeb
7
+ data.tar.gz: 92b1cb813ed3c54e18408fed0f79f2e9f4983672660f6ee1cf6c0ec9628577fbebf1055797e9d9a4c53ca5b293427c66d9c946a770a443e77b98c77c0937d244
@@ -10,7 +10,27 @@ on:
10
10
  permissions:
11
11
  contents: read
12
12
 
13
+ env:
14
+ FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
15
+
13
16
  jobs:
17
+ lint:
18
+ name: Lint
19
+ runs-on: ubuntu-latest
20
+
21
+ steps:
22
+ - name: Check out repository
23
+ uses: actions/checkout@v5
24
+
25
+ - name: Set up Ruby
26
+ uses: ruby/setup-ruby@v1
27
+ with:
28
+ ruby-version: "3.4"
29
+ bundler-cache: true
30
+
31
+ - name: Run StandardRB
32
+ run: bundle exec standardrb
33
+
14
34
  test:
15
35
  name: Ruby ${{ matrix.ruby }}
16
36
  runs-on: ubuntu-latest
@@ -32,6 +52,13 @@ jobs:
32
52
  ruby-version: ${{ matrix.ruby }}
33
53
  bundler-cache: true
34
54
 
35
- - name: Run test and lint suite
36
- run: bundle exec rake
55
+ - name: Run test suite
56
+ run: bundle exec rspec
57
+
58
+ - name: Upload coverage to Codecov
59
+ uses: codecov/codecov-action@v5
60
+ if: matrix.ruby == '3.4'
61
+ with:
62
+ token: ${{ secrets.CODECOV_TOKEN }}
63
+ files: coverage/.resultset.json
37
64
 
@@ -7,6 +7,7 @@ on:
7
7
 
8
8
  permissions:
9
9
  contents: write
10
+ id-token: write
10
11
 
11
12
  jobs:
12
13
  release:
@@ -66,10 +67,13 @@ jobs:
66
67
  RELEASE_TAG: ${{ github.ref_name }}
67
68
  run: echo "RubyGems already has version ${RELEASE_TAG#v}; skipping gem push."
68
69
 
70
+ - name: Configure RubyGems credentials (trusted publishing)
71
+ if: steps.rubygems.outputs.already_published != 'true'
72
+ uses: rubygems/configure-rubygems-credentials@v1.0.0
73
+
69
74
  - name: Publish gem to RubyGems
70
75
  if: steps.rubygems.outputs.already_published != 'true'
71
76
  env:
72
- GEM_HOST_API_KEY: ${{ secrets.RUBYGEMS_API_KEY }}
73
77
  RELEASE_TAG: ${{ github.ref_name }}
74
78
  run: |
75
79
  version="${RELEASE_TAG#v}"
data/CHANGELOG.md CHANGED
@@ -1,103 +1,147 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html)
7
+ from v1.0.0 onwards. Prior 0.x releases may include breaking changes between minor versions.
8
+
1
9
  ## [Unreleased]
2
10
 
11
+ ## [0.8.0] - 2026-05-21
12
+
13
+ ### Added
14
+
15
+ - Raise `ArgumentError` at definition time when `memoize` is called on a method that does not exist on the class — previously the error only surfaced at runtime when `super` had nothing to call
16
+ - 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
17
+ - `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`, and `lru_position`; returns `nil` when the entry is not cached
18
+ - Deprecation infrastructure: `SafeMemoize.deprecate(subject, message:, horizon:)` emits a structured `[SafeMemoize]` warning to stderr by default; configurable via `SafeMemoize.configure { |c| c.on_deprecation = ->(msg) { ... } }` to raise, log, or collect warnings
19
+ - `memoize_all only:` — symmetric counterpart to `except:`; explicitly lists the methods to memoize and skips all others; raises `ArgumentError` when both `only:` and `except:` are given
20
+ - 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 via `SafeMemoize.configure { |c| c.on_hook_error = ->(error, hook_type, cache_key) { ... } }` to raise, log, or silence
21
+
22
+ ## [0.7.0] - 2026-05-18
23
+
24
+ ### Added
25
+
26
+ - `memo_preload` to 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 order
27
+ - `on_memo_store` hook that fires whenever a value is written to the cache (miss, `warm_memo`, or `load_memo`); completes the full lifecycle hook set alongside `on_hit`, `on_miss`, `on_expire`, and `on_evict`
28
+ - `SafeMemoize.configure` for global default options — `default_ttl` and `default_max_size` apply to all subsequently memoized methods; per-call options override the global defaults
29
+ - `SafeMemoize.reset_configuration!` to restore all global defaults to `nil`
30
+ - `memo_touch` to reset the expiry clock on a cached entry without recomputing — accepts an optional `ttl:` override; returns `true` on success, `false` if the entry is not cached or already expired
31
+ - `shared_memo_age` class method to inspect how long ago a shared entry was cached
32
+ - `shared_memo_stale?` class method to check whether a shared entry's TTL has elapsed
33
+ - `key:` option on `memoize` for class-level cache key generation — calls whose key block returns the same value share one cache entry; instance-level `memoize_with_custom_key` still takes priority
34
+ - `memo_refresh` to force-recompute a cached entry and store the new value in one call
35
+ - `memo_age` to return how many seconds ago an entry was cached (`nil` if not cached or expired)
36
+ - `memo_stale?` to check whether a cached entry exists but its TTL has elapsed
37
+
38
+ ### Changed
39
+
40
+ - `cache_metrics_reset` now accepts an optional method name to clear stats for a single method only; calling without arguments still clears all metrics
41
+ - `shared:` support in `memoize_all` is now tested and documented (was already functional via `**options` passthrough)
42
+ - RBS type signatures updated for all new methods and the `Configuration` class
43
+
3
44
  ## [0.6.3] - 2026-05-18
4
45
 
46
+ ### Changed
47
+
5
48
  - Upgrade `softprops/action-gh-release` from v2 to v3 to resolve Node.js 20 deprecation warning in release workflow
6
49
 
7
50
  ## [0.6.2] - 2026-05-18
8
51
 
9
- - Achieve 100% line coverage across all lib files
10
- - Add SimpleCov filter to exclude `/spec` from coverage reporting
11
- - Add tests for `memo_ttl` in `CacheRecordMethods` covering nil, valid numeric, negative, and non-numeric inputs
12
- - Add tests for private `memo_cache_read` in `CacheStoreMethods` covering nil cache, live hit, and expired entry
13
- - Add tests for `memo_keys` / `memo_values` with custom-key entries, covering the `custom_key:` projection branch in `InspectionMethods`
14
- - Add missing error-case tests for `ReleaseTooling.update_version_file` (no VERSION constant) and `finalize_changelog` (no Unreleased heading)
52
+ ### Added
53
+
54
+ - 100% line coverage across all lib files — added tests for edge cases in `CacheRecordMethods`, `CacheStoreMethods`, `InspectionMethods`, and `ReleaseTooling`; added SimpleCov filter to exclude `/spec` from coverage reporting
15
55
 
16
56
  ## [0.6.1] - 2026-05-17
17
57
 
18
- - Fix `memo_keys` and `memo_values` showing `args: custom_key, kwargs: nil` for methods using `memoize_with_custom_key` — now surfaces as `custom_key:`
19
- - Refactor `cache_stats` / `cache_stats_for` to share aggregation logic via private helpers
58
+ ### Changed
59
+
60
+ - Refactored `cache_stats` / `cache_stats_for` to share aggregation logic via private helpers
61
+
62
+ ### Fixed
63
+
64
+ - `memo_keys` and `memo_values` showed `args: custom_key, kwargs: nil` for methods using `memoize_with_custom_key` — now correctly surfaces as `custom_key:`
20
65
 
21
66
  ## [0.6.0] - 2026-05-17
22
67
 
23
- - Fix TTL clock starting at `memoize` definition time instead of first method call
24
- - Fix metrics key silently dropping kwargs, causing methods that differ only in kwargs to share a metrics bucket
25
- - Fix stale LRU references remaining after expired entries are pruned
26
- - Add `ttl:` option to `warm_memo` so warmed entries can be given an expiry
27
- - Add `max_size:` support for `shared: true` memoization (class-level LRU eviction)
28
- - Add `ttl_refresh: true` option on `memoize` for sliding window TTL — resets expiry on every cache hit
29
- - Add `include_protected:` and `include_private:` options to `memoize_all`
30
- - Add `memo_ttl_remaining` for TTL introspection — returns seconds until expiry, `nil` for no TTL, `0` for uncached/expired
68
+ ### Added
69
+
70
+ - `ttl:` option on `warm_memo` so warmed entries can be given an expiry
71
+ - `max_size:` support for `shared: true` memoization (class-level LRU eviction)
72
+ - `ttl_refresh: true` option on `memoize` for sliding window TTL — resets the expiry clock on every cache hit so the entry only expires after a full TTL of inactivity
73
+ - `include_protected:` and `include_private:` options on `memoize_all`
74
+ - `memo_ttl_remaining` for TTL introspection — returns seconds until expiry, `nil` for no TTL, `0` for uncached or expired
75
+
76
+ ### Fixed
77
+
78
+ - TTL clock started at `memoize` definition time instead of at first method call
79
+ - Metrics key silently dropped kwargs, causing methods that differ only in kwargs to share a metrics bucket
80
+ - Stale LRU references remained in the order list after expired entries were pruned
31
81
 
32
82
  ## [0.5.0] - 2026-05-17
33
83
 
34
- - Drop support for Ruby 3.2 (EOL); minimum required version is now Ruby 3.3
84
+ ### Removed
85
+
86
+ - Support for Ruby 3.2 (EOL); minimum required version is now Ruby 3.3
35
87
 
36
88
  ## [0.4.0] - 2026-05-17
37
89
 
38
- - Add `warm_memo`, `dump_memo`, and `load_memo` for cache warm-up and persistence
39
- - `warm_memo(:method, *args, **kwargs) { value }` — pre-populates a cache entry via block without calling the method
40
- - `dump_memo` / `dump_memo(:method)` — exports live cached entries as a plain `{[method, args, kwargs] => value}` hash
41
- - `load_memo(snapshot)` merges a snapshot into the cache; loaded entries have no TTL
42
- - Expired entries are excluded from `dump_memo` output
43
- - Add `shared: true` option on `memoize` to store results on the class instead of per-instance
44
- - All instances share one cache; the method is computed only once regardless of how many objects exist
45
- - Class-level invalidation: `reset_shared_memo`, `reset_all_shared_memos`
46
- - Class-level inspection: `shared_memoized?`, `shared_memo_count`
47
- - Supports `ttl:`, `if:`, and `unless:` options
48
- - Instance hooks (`on_memo_hit`, `on_memo_miss`, `on_memo_expire`) fire on the calling instance
49
- - Add `memoize_all` to memoize every public method defined on the class in one call
50
- - Accepts all options supported by `memoize` (`ttl:`, `max_size:`, `if:`, `unless:`)
51
- - `except:` option to skip specific methods by name
52
- - Only affects public methods defined directly on the class
53
- - Add `on_memo_miss` hook that fires on every cache miss, completing the full lifecycle hook set alongside `on_memo_hit`, `on_memo_evict`, and `on_memo_expire`
90
+ ### Added
91
+
92
+ - `warm_memo`, `dump_memo`, and `load_memo` for cache warm-up and persistence pre-populate entries without calling the method, export live entries as a plain hash, and restore from a snapshot
93
+ - `shared: true` option on `memoize` to store results on the class instead of per-instance includes `reset_shared_memo`, `reset_all_shared_memos`, `shared_memoized?`, and `shared_memo_count`; supports `ttl:`, `if:`, and `unless:`
94
+ - `memoize_all` to memoize every public method defined on the class in one call — accepts all `memoize` options plus `except:` to skip specific methods
95
+ - `on_memo_miss` hook that fires on every cache miss, completing the full lifecycle hook set
54
96
 
55
97
  ## [0.3.0] - 2026-05-15
56
98
 
57
- - Add `on_memo_hit` hook that fires on every cache hit, completing the lifecycle API alongside `on_memo_expire` and `on_memo_evict`
58
- - Add conditional memoization via `if:` and `unless:` options on `memoize`
59
- - `if: ->(result) { ... }` only caches when the lambda returns truthy
60
- - `unless: ->(result) { ... }` — skips caching when the lambda returns truthy
61
- - Uncached calls recompute on every invocation until the condition is met
62
- - Compatible with `ttl:`, `max_size:`, hooks, and all inspection APIs
63
- - Add LRU cache size limit via `max_size:` option on `memoize`
64
- - Evicts the least-recently-used entry per method when the limit is reached
65
- - Cache hits promote entries to most-recently-used, preventing premature eviction
66
- - Fires the existing `on_evict` hook for LRU-evicted entries
67
- - Self-healing: stale LRU references left by `reset_memo` are pruned automatically
68
- - Compatible with `ttl:` option and all existing inspection/reset APIs
69
- - Thread-safe under concurrent access
99
+ ### Added
100
+
101
+ - `on_memo_hit` hook that fires on every cache hit
102
+ - Conditional memoization via `if:` and `unless:` predicates on `memoize`uncached calls recompute on every invocation until the condition is satisfied; composes with `ttl:`, `max_size:`, and hooks
103
+ - LRU cache size limit via `max_size:` on `memoize` evicts the least-recently-used entry when the limit is reached; cache hits promote entries; fires `on_evict`; thread-safe
70
104
 
71
105
  ## [0.2.0] - 2026-05-14
72
106
 
73
- - Add optional TTL expiration support for memoized entries
74
- - Add cache invalidation/expiration hooks for custom handlers
75
- - `on_memo_expire` hook fires when TTL entries expire
76
- - `on_memo_evict` hook fires when manually resetting cache entries
77
- - `clear_memo_hooks` to remove registered hooks
78
- - Add cache statistics and monitoring capabilities
79
- - `cache_stats` for comprehensive cache metrics
80
- - `cache_stats_for(method_name)` for per-method statistics
81
- - `cache_hit_rate` and `cache_miss_rate` for performance analysis
82
- - `cache_metrics_reset` to clear collected metrics
83
- - Add manual cache key generation support
84
- - `memoize_with_custom_key` to define custom cache key logic
85
- - `clear_custom_keys` to remove custom key generators
86
- - Support for complex and computed keys based on arguments
107
+ ### Added
108
+
109
+ - Optional TTL expiration for memoized entries
110
+ - `on_memo_expire` and `on_memo_evict` lifecycle hooks; `clear_memo_hooks` to remove registered hooks
111
+ - Cache metrics: `cache_stats`, `cache_stats_for`, `cache_hit_rate`, `cache_miss_rate`, and `cache_metrics_reset`
112
+ - Custom cache key generation via `memoize_with_custom_key` and `clear_custom_keys`
87
113
 
88
114
  ## [0.1.2] - 2026-05-13
89
115
 
90
- - Preserve public, protected, and private visibility for memoized methods
91
- - Allow reset_memo to clear one cached argument combination or all entries for a method
92
- - Add a memoized? helper for checking whether a method call is already cached
93
- - Add a memo_count helper for inspecting cache size per instance or method
94
- - Add a memo_keys helper for inspecting cached argument signatures
95
- - Add a memo_values helper for inspecting cached signatures and their values
116
+ ### Added
117
+
118
+ - Method visibility preservation (public, protected, private) for memoized methods
119
+ - Targeted `reset_memo` clear one cached argument combination or all entries for a method
120
+ - `memoized?` helper to check whether a specific call is cached
121
+ - `memo_count`, `memo_keys`, and `memo_values` helpers for cache introspection
96
122
 
97
123
  ## [0.1.1] - 2026-05-13
98
124
 
99
- - Add automated release tooling plus a GitHub Actions workflow for RubyGems publishing and GitHub releases
125
+ ### Added
126
+
127
+ - Automated release tooling (`bin/release`) and GitHub Actions workflow for RubyGems publishing and GitHub releases
100
128
 
101
129
  ## [0.1.0] - 2026-02-26
102
130
 
131
+ ### Added
132
+
103
133
  - Initial release
134
+
135
+ [Unreleased]: https://github.com/eclectic-coding/safe_memoize/compare/v0.7.0...HEAD
136
+ [0.7.0]: https://github.com/eclectic-coding/safe_memoize/compare/v0.6.3...v0.7.0
137
+ [0.6.3]: https://github.com/eclectic-coding/safe_memoize/compare/v0.6.2...v0.6.3
138
+ [0.6.2]: https://github.com/eclectic-coding/safe_memoize/compare/v0.6.1...v0.6.2
139
+ [0.6.1]: https://github.com/eclectic-coding/safe_memoize/compare/v0.6.0...v0.6.1
140
+ [0.6.0]: https://github.com/eclectic-coding/safe_memoize/compare/v0.5.0...v0.6.0
141
+ [0.5.0]: https://github.com/eclectic-coding/safe_memoize/compare/v0.4.0...v0.5.0
142
+ [0.4.0]: https://github.com/eclectic-coding/safe_memoize/compare/v0.3.0...v0.4.0
143
+ [0.3.0]: https://github.com/eclectic-coding/safe_memoize/compare/v0.2.0...v0.3.0
144
+ [0.2.0]: https://github.com/eclectic-coding/safe_memoize/compare/v0.1.2...v0.2.0
145
+ [0.1.2]: https://github.com/eclectic-coding/safe_memoize/compare/v0.1.1...v0.1.2
146
+ [0.1.1]: https://github.com/eclectic-coding/safe_memoize/compare/v0.1.0...v0.1.1
147
+ [0.1.0]: https://github.com/eclectic-coding/safe_memoize/releases/tag/v0.1.0
data/README.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # SafeMemoize
2
2
 
3
+ [![CI](https://github.com/eclectic-coding/safe_memoize/actions/workflows/ci.yml/badge.svg)](https://github.com/eclectic-coding/safe_memoize/actions/workflows/ci.yml)
4
+ [![Gem Version](https://img.shields.io/gem/v/safe_memoize)](https://rubygems.org/gems/safe_memoize)
5
+ [![Total Downloads](https://img.shields.io/gem/dt/safe_memoize)](https://rubygems.org/gems/safe_memoize)
6
+ [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%203.3-CC342D)](https://www.ruby-lang.org)
7
+ [![codecov](https://codecov.io/gh/eclectic-coding/safe_memoize/graph/badge.svg)](https://codecov.io/gh/eclectic-coding/safe_memoize)
8
+
3
9
  Thread-safe memoization for Ruby that correctly handles `nil` and `false` values.
4
10
 
5
11
  SafeMemoize is a production-ready, zero-dependency memoization library for Ruby. It wraps methods with a `prepend`-based cache that handles everything the standard `||=` idiom gets wrong: `nil` and `false` return values are cached correctly, per-argument result maps eliminate redundant computation for parameterized methods, and a per-instance `Mutex` with double-check locking makes the whole thing safe under concurrent load.
@@ -562,6 +568,10 @@ To preview the changelog/version update without changing anything, use:
562
568
  bin/release 0.1.1 --dry-run
563
569
  ```
564
570
 
571
+ ## Roadmap
572
+
573
+ See [ROADMAP.md](ROADMAP.md) for the planned path from v0.7.0 to v1.0.0 and beyond, including upcoming features, API stability goals, and the versioning policy.
574
+
565
575
  ## Contributing
566
576
 
567
577
  Bug reports and pull requests are welcome on GitHub at https://github.com/eclectic-coding/safe_memoize.
data/ROADMAP.md ADDED
@@ -0,0 +1,92 @@
1
+ # SafeMemoize Roadmap
2
+
3
+ This document tracks the planned evolution of SafeMemoize through v1.0.0 and beyond. Items are grouped by release milestone; ordering within a milestone reflects priority, not a strict implementation sequence.
4
+
5
+ ---
6
+
7
+ ## v0.9.0 — Observability & Ecosystem Integration
8
+
9
+ *Goal: make SafeMemoize a first-class citizen in Rails/ActiveSupport stacks and in observability pipelines.*
10
+
11
+ | Feature | Description | Status |
12
+ |---|---|---|
13
+ | ActiveSupport::Notifications integration | Emit `cache.hit`, `cache.miss`, `cache.evict`, and `cache.expire` events when ActiveSupport is available (opt-in via configuration) | Planned |
14
+ | StatsD adapter | Thin optional module (`SafeMemoize::Adapters::StatsD`) that routes lifecycle hooks to a StatsD client with sensible metric names and tags | Planned |
15
+ | OpenTelemetry spans | Optional adapter (`SafeMemoize::Adapters::OpenTelemetry`) wrapping computation time in a trace span for distributed tracing pipelines | Planned |
16
+ | Rails request-scope helper | Guide + optional mixin for resetting instance memos at the end of each request (controller concern, middleware, or Active Model pattern) | Planned |
17
+ | Formal benchmark suite | `benchmarks/` directory with comparisons against `memery`, `memo_wise`, and raw `||=`, covering single-threaded throughput and contention under concurrent load | Planned |
18
+ | Concurrency stress tests | Dedicated spec suite hammering shared-cache paths and LRU eviction under high thread counts to surface race conditions | Planned |
19
+
20
+ ---
21
+
22
+ ## v1.0.0 — Stable API
23
+
24
+ *Goal: declare a stable, semver-governed public API that downstream code can depend on with confidence.*
25
+
26
+ | Feature | Description | Status |
27
+ |---|---|---|
28
+ | Semantic versioning guarantee | Document which constants, methods, and option keys are public API; breaking changes require a major bump henceforth | Planned |
29
+ | Complete RBS + Sorbet signatures | Cover all public methods including overloads for optional keyword arguments; publish `.rbi` stubs as a companion package if demand warrants | Planned |
30
+ | Full API reference | Generated documentation hosted on RubyDoc or a dedicated docs site; all public methods documented with parameter types, return values, and usage examples | Planned |
31
+ | Ractor compatibility audit | Investigate and either support Ractor-compatible operation (Mutex replacement, shared-cache storage) or document the limitation clearly | Planned |
32
+ | Ruby version policy | Formalise the supported Ruby version window and cadence for dropping EOL versions | Planned |
33
+ | Deprecation sweep | Resolve or formally deprecate any unstable internal APIs before the stable release | Planned |
34
+ | Upgrade guide | Document all breaking changes from 0.x and provide a migration path for users of deprecated behaviour | Planned |
35
+
36
+ ---
37
+
38
+ ## v1.1.0 — Pluggable Cache Stores
39
+
40
+ *Goal: allow the in-process hash cache to be swapped for an external store, enabling cross-process and distributed memoization.*
41
+
42
+ | Feature | Description | Status |
43
+ |---|---|---|
44
+ | Cache store adapter interface | Define a minimal read/write/delete/clear/keys contract that external backends must implement | Planned |
45
+ | `store:` option on `memoize` | Accept any store adapter object; defaults to the existing in-process hash store | Planned |
46
+ | Redis adapter | Reference implementation (`SafeMemoize::Stores::Redis`) with TTL, LRU-like expiry, and serialization handled transparently | Planned |
47
+ | Rails.cache adapter | Thin wrapper around `ActiveSupport::Cache::Store` for projects already using a configured Rails cache | Planned |
48
+ | Global default store | Set via `SafeMemoize.configure` — applies a default store to every memoized method without per-call configuration | Planned |
49
+
50
+ ---
51
+
52
+ ## v1.2.0 — Async & Fiber-Safe Memoization
53
+
54
+ *Goal: first-class support for Fiber-based concurrency frameworks (Async, Falcon, Rails async controllers).*
55
+
56
+ | Feature | Description | Status |
57
+ |---|---|---|
58
+ | Fiber-local memoization mode | `memoize :method, fiber_local: true` stores results in `Fiber[:safe_memoize_cache]` rather than instance variables, giving each fiber its own isolated cache automatically reset when the fiber terminates | Planned |
59
+ | Ractor-compatible shared cache | Revisit `shared: true` using `Ractor::TVar` or shareable frozen objects so class-level caches work across Ractors | Planned |
60
+ | concurrent-ruby integration | Optional adapter using `Concurrent::Map` and `Concurrent::ReentrantReadWriteLock` as a drop-in replacement for `Mutex` where higher read-concurrency is desirable | Planned |
61
+
62
+ ---
63
+
64
+ ## v2.0.0 — Next Generation (Long Horizon)
65
+
66
+ *Goal: incorporate real-world usage feedback, clean up accumulated API surface, and open a path for advanced extension.*
67
+
68
+ | Feature | Description | Status |
69
+ |---|---|---|
70
+ | Plugin / extension architecture | A formal `SafeMemoize::Extension` API so third-party gems can add new options, hooks, or store adapters without monkey-patching | Planned |
71
+ | DSL refinements | Evaluate alternative syntax proposals (`memoize_method`, block form, annotation approach) based on community feedback; introduce the preferred form with a migration path from the current API | Planned |
72
+ | Cross-instance cache sharing | Beyond the class-level `shared: true`, support explicitly named shared caches that span unrelated classes | Planned |
73
+ | Cache namespacing | Allow a namespace prefix on all keys for multi-tenant or versioned deployments (especially useful with external stores) | Planned |
74
+ | Automatic cache busting | Optional integration with ActiveRecord's `updated_at` timestamp so object mutations automatically invalidate their own cached entries | Planned |
75
+
76
+ ---
77
+
78
+ ## Versioning policy
79
+
80
+ SafeMemoize follows [Semantic Versioning](https://semver.org/) from v1.0.0 onwards:
81
+
82
+ - **Patch** (1.x.**y**) — bug fixes; no API changes
83
+ - **Minor** (1.**x**.0) — additive features; backward-compatible
84
+ - **Major** (**x**.0.0) — breaking changes; migration guide published
85
+
86
+ 0.x releases may include breaking changes between minor versions.
87
+
88
+ ---
89
+
90
+ ## Contributing
91
+
92
+ Ideas, bug reports, and pull requests are welcome. Open an issue at <https://github.com/eclectic-coding/safe_memoize/issues> to discuss a feature before building it. If you are picking up a roadmap item, mention the milestone in your PR so it can be tracked against this document.
@@ -26,5 +26,11 @@ module SafeMemoize
26
26
  def _reset_cache_metrics
27
27
  @__safe_memo_metrics__ = {}
28
28
  end
29
+
30
+ def _reset_cache_metrics_for(method_name)
31
+ return unless defined?(@__safe_memo_metrics__) && @__safe_memo_metrics__
32
+
33
+ @__safe_memo_metrics__.delete_if { |key, _| key[0] == method_name }
34
+ end
29
35
  end
30
36
  end
@@ -22,7 +22,7 @@ module SafeMemoize
22
22
  end
23
23
 
24
24
  def memo_record(value, expires_at:)
25
- {value: value, expires_at: expires_at}
25
+ {value: value, expires_at: expires_at, cached_at: Process.clock_gettime(Process::CLOCK_MONOTONIC)}
26
26
  end
27
27
 
28
28
  def memo_record_value(record)
@@ -2,10 +2,19 @@
2
2
 
3
3
  module SafeMemoize
4
4
  module ClassMethods
5
- def memoize(method_name, ttl: nil, max_size: nil, ttl_refresh: false, if: nil, unless: nil, shared: false)
5
+ def memoize(method_name, ttl: nil, max_size: nil, ttl_refresh: false, if: nil, unless: nil, shared: false, key: nil)
6
6
  method_name = method_name.to_sym
7
+
8
+ unless method_defined?(method_name) || private_method_defined?(method_name) || protected_method_defined?(method_name)
9
+ raise ArgumentError, "cannot memoize :#{method_name} — no instance method with that name is defined on #{self}"
10
+ end
11
+
7
12
  visibility = memoized_method_visibility(method_name)
8
13
 
14
+ config = SafeMemoize.configuration
15
+ ttl = config.default_ttl if ttl.nil?
16
+ max_size = config.default_max_size if max_size.nil?
17
+
9
18
  # :if and :unless are reserved Ruby keywords, so they can't be referenced
10
19
  # as local variables directly. binding.local_variable_get is the only way
11
20
  # to read keyword arguments with those names inside the method body.
@@ -38,6 +47,9 @@ module SafeMemoize
38
47
  end
39
48
  raise ArgumentError, ":if must be callable" if cond_if && !cond_if.respond_to?(:call)
40
49
  raise ArgumentError, ":unless must be callable" if cond_unless && !cond_unless.respond_to?(:call)
50
+ raise ArgumentError, ":key must be callable" if key && !key.respond_to?(:call)
51
+
52
+ __safe_memo_class_key_generators__[method_name] = key if key
41
53
 
42
54
  # Normalize to a single "should cache?" predicate
43
55
  condition = if cond_if
@@ -79,7 +91,7 @@ module SafeMemoize
79
91
  value = super(*args, **kwargs)
80
92
  elapsed_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
81
93
 
82
- new_record = {value: value, expires_at: memo_expires_at(ttl)}
94
+ new_record = memo_record(value, expires_at: memo_expires_at(ttl))
83
95
 
84
96
  if !condition || condition.call(value)
85
97
  if max_size
@@ -97,6 +109,7 @@ module SafeMemoize
97
109
  lru = klass.send(:__safe_memo_shared_lru_order__)[method_name] ||= {}
98
110
  lru[cache_key] = true
99
111
  end
112
+ call_memo_hooks(:on_store, cache_key, new_record)
100
113
  end
101
114
 
102
115
  record_cache_miss(method_name, args, kwargs, elapsed_time)
@@ -143,6 +156,7 @@ module SafeMemoize
143
156
  @__safe_memo_cache__ ||= {}
144
157
  @__safe_memo_cache__[cache_key] = new_record
145
158
  lru_touch(method_name, cache_key) if max_size
159
+ call_memo_hooks(:on_store, cache_key, new_record)
146
160
  end
147
161
  record_cache_miss(method_name, args, kwargs, elapsed_time)
148
162
  call_memo_hooks(:on_miss, cache_key, new_record)
@@ -166,6 +180,7 @@ module SafeMemoize
166
180
  with_memo_lock do
167
181
  record_cache_miss(method_name, args, kwargs, elapsed_time)
168
182
  new_record = memo_cache_record(cache_key)
183
+ call_memo_hooks(:on_store, cache_key, new_record)
169
184
  call_memo_hooks(:on_miss, cache_key, new_record)
170
185
  end
171
186
 
@@ -225,8 +240,50 @@ module SafeMemoize
225
240
  end
226
241
  end
227
242
 
228
- def memoize_all(except: [], include_protected: false, include_private: false, **options)
243
+ def shared_memo_age(method_name, *args, **kwargs)
244
+ method_name = method_name.to_sym
245
+ cache_key = [method_name, args, kwargs]
246
+
247
+ __safe_memo_shared_mutex__.synchronize do
248
+ cache = @__safe_memo_shared_cache__
249
+ return nil unless cache
250
+
251
+ record = cache[cache_key]
252
+ return nil unless record
253
+
254
+ now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
255
+ return nil if record[:expires_at] && record[:expires_at] <= now
256
+
257
+ cached_at = record[:cached_at]
258
+ return nil unless cached_at
259
+
260
+ (now - cached_at).round(6)
261
+ end
262
+ end
263
+
264
+ def shared_memo_stale?(method_name, *args, **kwargs)
265
+ method_name = method_name.to_sym
266
+ cache_key = [method_name, args, kwargs]
267
+
268
+ __safe_memo_shared_mutex__.synchronize do
269
+ cache = @__safe_memo_shared_cache__
270
+ return false unless cache
271
+
272
+ record = cache[cache_key]
273
+ return false unless record
274
+
275
+ expires_at = record[:expires_at]
276
+ return false unless expires_at
277
+
278
+ expires_at <= Process.clock_gettime(Process::CLOCK_MONOTONIC)
279
+ end
280
+ end
281
+
282
+ def memoize_all(except: [], only: [], include_protected: false, include_private: false, **options)
283
+ raise ArgumentError, "cannot specify both :only and :except" if only.any? && except.any?
284
+
229
285
  excluded = Array(except).map(&:to_sym)
286
+ included = Array(only).map(&:to_sym)
230
287
 
231
288
  methods = public_instance_methods(false)
232
289
  methods |= protected_instance_methods(false) if include_protected
@@ -234,6 +291,7 @@ module SafeMemoize
234
291
 
235
292
  methods.each do |method_name|
236
293
  next if excluded.include?(method_name)
294
+ next if included.any? && !included.include?(method_name)
237
295
 
238
296
  memoize(method_name, **options)
239
297
  end
@@ -253,6 +311,10 @@ module SafeMemoize
253
311
  @__safe_memo_shared_lru_order__ ||= {}
254
312
  end
255
313
 
314
+ def __safe_memo_class_key_generators__
315
+ @__safe_memo_class_key_generators__ ||= {}
316
+ end
317
+
256
318
  def memoized_method_visibility(method_name)
257
319
  return :private if private_method_defined?(method_name)
258
320
  return :protected if protected_method_defined?(method_name)
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SafeMemoize
4
+ class Configuration
5
+ attr_accessor :default_ttl, :default_max_size, :on_deprecation, :on_hook_error
6
+
7
+ def initialize
8
+ @default_ttl = nil
9
+ @default_max_size = nil
10
+ @on_deprecation = nil
11
+ @on_hook_error = nil
12
+ end
13
+ end
14
+ end
@@ -18,16 +18,13 @@ module SafeMemoize
18
18
  def compute_cache_key(method_name, args, kwargs)
19
19
  method_name = method_name.to_sym
20
20
 
21
- # Check if a custom key generator is registered
22
- custom_key_block = custom_key_store[method_name]
23
-
24
- if custom_key_block
25
- # Call the custom key generator with args and kwargs
26
- custom_key = custom_key_block.call(*args, **kwargs)
27
- # Wrap in a standard format: [method, custom_key]
28
- [method_name, custom_key]
21
+ # Instance-level key generator takes priority over class-level
22
+ key_block = custom_key_store[method_name] ||
23
+ self.class.send(:__safe_memo_class_key_generators__)[method_name]
24
+
25
+ if key_block
26
+ [method_name, key_block.call(*args, **kwargs)]
29
27
  else
30
- # Use default key generation
31
28
  safe_memo_cache_key(method_name, args, kwargs)
32
29
  end
33
30
  end
@@ -5,13 +5,13 @@ module SafeMemoize
5
5
  private
6
6
 
7
7
  def memo_hook_store
8
- @__safe_memo_hooks__ ||= {on_expire: [], on_evict: [], on_hit: [], on_miss: []}
8
+ @__safe_memo_hooks__ ||= {on_expire: [], on_evict: [], on_hit: [], on_miss: [], on_store: []}
9
9
  end
10
10
 
11
11
  def register_memo_hook(hook_type, &block)
12
12
  raise ArgumentError, "block required" unless block
13
13
 
14
- valid_hooks = [:on_expire, :on_evict, :on_hit, :on_miss]
14
+ valid_hooks = [:on_expire, :on_evict, :on_hit, :on_miss, :on_store]
15
15
  raise ArgumentError, "invalid hook type: #{hook_type}" unless valid_hooks.include?(hook_type)
16
16
 
17
17
  memo_hook_store[hook_type] << block
@@ -19,14 +19,23 @@ module SafeMemoize
19
19
 
20
20
  def call_memo_hooks(hook_type, cache_key, record)
21
21
  hooks = memo_hook_store[hook_type] || []
22
- hooks.each { |hook| hook.call(cache_key, record) }
22
+ hooks.each do |hook|
23
+ hook.call(cache_key, record)
24
+ rescue => error
25
+ handler = SafeMemoize.configuration.on_hook_error
26
+ if handler
27
+ handler.call(error, hook_type, cache_key)
28
+ else
29
+ warn "[SafeMemoize] Hook error in #{hook_type}: #{error.message}"
30
+ end
31
+ end
23
32
  end
24
33
 
25
34
  def _clear_memo_hooks(hook_type = nil)
26
35
  if hook_type
27
36
  memo_hook_store[hook_type] = []
28
37
  else
29
- @__safe_memo_hooks__ = {on_expire: [], on_evict: [], on_hit: [], on_miss: []}
38
+ @__safe_memo_hooks__ = {on_expire: [], on_evict: [], on_hit: [], on_miss: [], on_store: []}
30
39
  end
31
40
  end
32
41
  end
@@ -69,7 +69,20 @@ module SafeMemoize
69
69
  end
70
70
 
71
71
  def safe_memo_cache_key(method_name, args, kwargs)
72
- [method_name.to_sym, args, kwargs]
72
+ [method_name.to_sym, deep_freeze_copy(args), deep_freeze_copy(kwargs)]
73
+ end
74
+
75
+ def deep_freeze_copy(obj)
76
+ case obj
77
+ when Array
78
+ obj.map { |e| deep_freeze_copy(e) }.freeze
79
+ when Hash
80
+ obj.each_with_object({}) { |(k, v), h| h[deep_freeze_copy(k)] = deep_freeze_copy(v) }.freeze
81
+ when String
82
+ -obj
83
+ else
84
+ obj
85
+ end
73
86
  end
74
87
  end
75
88
  end
@@ -75,6 +75,12 @@ module SafeMemoize
75
75
  register_memo_hook(:on_miss, &block)
76
76
  end
77
77
 
78
+ def on_memo_store(&block)
79
+ raise ArgumentError, "block required" unless block
80
+
81
+ register_memo_hook(:on_store, &block)
82
+ end
83
+
78
84
  def clear_memo_hooks(hook_type = nil)
79
85
  with_memo_lock do
80
86
  _clear_memo_hooks(hook_type)
@@ -90,12 +96,21 @@ module SafeMemoize
90
96
 
91
97
  with_memo_lock do
92
98
  @__safe_memo_cache__ ||= {}
93
- @__safe_memo_cache__[cache_key] = memo_record(value, expires_at: memo_expires_at(ttl))
99
+ record = memo_record(value, expires_at: memo_expires_at(ttl))
100
+ @__safe_memo_cache__[cache_key] = record
101
+ call_memo_hooks(:on_store, cache_key, record)
94
102
  end
95
103
 
96
104
  value
97
105
  end
98
106
 
107
+ def memo_preload(method_name, *arg_sets)
108
+ method_name = method_name.to_sym
109
+ arg_sets.map do |args|
110
+ send(method_name, *Array(args))
111
+ end
112
+ end
113
+
99
114
  def dump_memo(method_name = nil)
100
115
  method_name = method_name&.to_sym
101
116
 
@@ -113,13 +128,74 @@ module SafeMemoize
113
128
  with_memo_lock do
114
129
  @__safe_memo_cache__ ||= {}
115
130
  snapshot.each do |cache_key, value|
116
- @__safe_memo_cache__[cache_key] = memo_record(value, expires_at: nil)
131
+ record = memo_record(value, expires_at: nil)
132
+ @__safe_memo_cache__[cache_key] = record
133
+ call_memo_hooks(:on_store, cache_key, record)
117
134
  end
118
135
  end
119
136
 
120
137
  nil
121
138
  end
122
139
 
140
+ def memo_touch(method_name, *args, ttl: nil, **kwargs)
141
+ method_name = method_name.to_sym
142
+ cache_key = safe_memo_cache_key(method_name, args, kwargs)
143
+
144
+ with_memo_lock do
145
+ cache = memo_cache_or_nil
146
+ return false unless cache
147
+
148
+ record = cache[cache_key]
149
+ return false unless record && memo_record_live?(record)
150
+
151
+ now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
152
+
153
+ effective_ttl = if ttl
154
+ ttl
155
+ elsif record[:expires_at] && record[:cached_at]
156
+ record[:expires_at] - record[:cached_at]
157
+ end
158
+
159
+ record[:expires_at] = effective_ttl ? now + effective_ttl : nil
160
+ record[:cached_at] = now
161
+ true
162
+ end
163
+ end
164
+
165
+ def memo_refresh(method_name, *args, **kwargs)
166
+ method_name = method_name.to_sym
167
+ reset_memo(method_name, *args, **kwargs)
168
+ send(method_name, *args, **kwargs)
169
+ end
170
+
171
+ def memo_age(method_name, *args, **kwargs)
172
+ cache_key = safe_memo_cache_key(method_name, args, kwargs)
173
+
174
+ with_memo_lock do
175
+ record = memo_cache_record(cache_key)
176
+ return nil unless record
177
+
178
+ cached_at = record[:cached_at]
179
+ return nil unless cached_at
180
+
181
+ (Process.clock_gettime(Process::CLOCK_MONOTONIC) - cached_at).round(6)
182
+ end
183
+ end
184
+
185
+ def memo_stale?(method_name, *args, **kwargs)
186
+ cache_key = safe_memo_cache_key(method_name, args, kwargs)
187
+
188
+ with_memo_lock do
189
+ cache = memo_cache_or_nil
190
+ return false unless cache
191
+
192
+ record = cache[cache_key]
193
+ return false unless record
194
+
195
+ !memo_record_live?(record)
196
+ end
197
+ end
198
+
123
199
  def reset_memo(method_name, *args, **kwargs)
124
200
  method_name = method_name.to_sym
125
201
 
@@ -150,5 +226,48 @@ module SafeMemoize
150
226
  lru_clear_all
151
227
  end
152
228
  end
229
+
230
+ def memo_inspect(method_name, *args, **kwargs)
231
+ method_name = method_name.to_sym
232
+ cache_key = compute_cache_key(method_name, args, kwargs)
233
+
234
+ with_memo_lock do
235
+ record = memo_cache_record(cache_key)
236
+ return nil unless record
237
+
238
+ now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
239
+
240
+ ttl_remaining = if record[:expires_at]
241
+ remaining = record[:expires_at] - now
242
+ (remaining > 0) ? remaining.round(6) : 0
243
+ end
244
+
245
+ age = (now - record[:cached_at]).round(6) if record[:cached_at]
246
+
247
+ metrics_key = safe_memo_cache_key(method_name, args, kwargs)
248
+ entry_metrics = memo_metrics_store[metrics_key] || {hits: 0, misses: 0}
249
+
250
+ custom_key = (cache_key.length == 2) ? cache_key[1] : nil
251
+
252
+ lru_position = begin
253
+ method_lru = lru_order_store[method_name]
254
+ if method_lru&.key?(cache_key)
255
+ keys = method_lru.keys
256
+ keys.length - keys.index(cache_key)
257
+ end
258
+ end
259
+
260
+ {
261
+ cached: true,
262
+ value: memo_record_value(record),
263
+ hits: entry_metrics[:hits],
264
+ misses: entry_metrics[:misses],
265
+ ttl_remaining: ttl_remaining,
266
+ age: age,
267
+ custom_key: custom_key,
268
+ lru_position: lru_position
269
+ }
270
+ end
271
+ end
153
272
  end
154
273
  end
@@ -30,9 +30,13 @@ module SafeMemoize
30
30
  cache_stats[:miss_rate]
31
31
  end
32
32
 
33
- def cache_metrics_reset
33
+ def cache_metrics_reset(method_name = nil)
34
34
  with_memo_lock do
35
- _reset_cache_metrics
35
+ if method_name
36
+ _reset_cache_metrics_for(method_name.to_sym)
37
+ else
38
+ _reset_cache_metrics
39
+ end
36
40
  end
37
41
  end
38
42
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SafeMemoize
4
- VERSION = "0.6.3"
4
+ VERSION = "0.8.0"
5
5
  end
data/lib/safe_memoize.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "safe_memoize/version"
4
+ require_relative "safe_memoize/configuration"
4
5
  require_relative "safe_memoize/class_methods"
5
6
  require_relative "safe_memoize/public_methods"
6
7
  require_relative "safe_memoize/cache_store_methods"
@@ -22,4 +23,22 @@ module SafeMemoize
22
23
  def self.prepended(base)
23
24
  base.extend(ClassMethods)
24
25
  end
26
+
27
+ def self.configure
28
+ yield configuration
29
+ end
30
+
31
+ def self.configuration
32
+ @configuration ||= Configuration.new
33
+ end
34
+
35
+ def self.reset_configuration!
36
+ @configuration = Configuration.new
37
+ end
38
+
39
+ def self.deprecate(subject, message:, horizon:)
40
+ text = "[SafeMemoize] #{subject} is deprecated and will be removed in #{horizon}. #{message}"
41
+ handler = configuration.on_deprecation
42
+ handler ? handler.call(text) : warn(text)
43
+ end
25
44
  end
data/sig/safe_memoize.rbs CHANGED
@@ -5,7 +5,7 @@ module SafeMemoize
5
5
  type default_memo_key = [Symbol, Array[untyped], Hash[Symbol, untyped]]
6
6
  type custom_memo_key = [Symbol, untyped]
7
7
  type memo_key = default_memo_key | custom_memo_key
8
- type memo_record = { value: untyped, expires_at: Float? }
8
+ type memo_record = { value: untyped, expires_at: Float?, cached_at: Float }
9
9
 
10
10
  @__safe_memo_cache__: Hash[memo_key, memo_record]?
11
11
  @__safe_memo_mutex__: Mutex?
@@ -14,20 +14,36 @@ module SafeMemoize
14
14
  @__safe_memo_shared_lru_order__: Hash[Symbol, Hash[memo_key, true]]?
15
15
 
16
16
  def self.prepended: (Class base) -> void
17
+ def self.configure: () { (Configuration) -> void } -> void
18
+ def self.configuration: () -> Configuration
19
+ def self.reset_configuration!: () -> Configuration
20
+ def self.deprecate: (String subject, message: String, horizon: String) -> void
21
+
22
+ class Configuration
23
+ attr_accessor default_ttl: Numeric?
24
+ attr_accessor default_max_size: Integer?
25
+ attr_accessor on_deprecation: (^(String message) -> void)?
26
+ attr_accessor on_hook_error: (^(Exception error, Symbol hook_type, untyped cache_key) -> void)?
27
+
28
+ def initialize: () -> void
29
+ end
17
30
 
18
31
  module ClassMethods
19
- def memoize: (Symbol | String method_name, ?ttl: Numeric?, ?max_size: Integer?, ?ttl_refresh: bool, ?if: (^(untyped result) -> boolish)?, ?unless: (^(untyped result) -> boolish)?, ?shared: bool) -> void
20
- def memoize_all: (?except: Array[Symbol | String], ?include_protected: bool, ?include_private: bool, ?ttl: Numeric?, ?max_size: Integer?, ?if: (^(untyped result) -> boolish)?, ?unless: (^(untyped result) -> boolish)?) -> void
32
+ def memoize: (Symbol | String method_name, ?ttl: Numeric?, ?max_size: Integer?, ?ttl_refresh: bool, ?if: (^(untyped result) -> boolish)?, ?unless: (^(untyped result) -> boolish)?, ?shared: bool, ?key: (^(*untyped args, **untyped kwargs) -> untyped)?) -> void
33
+ def memoize_all: (?except: Array[Symbol | String], ?only: Array[Symbol | String], ?include_protected: bool, ?include_private: bool, ?ttl: Numeric?, ?max_size: Integer?, ?if: (^(untyped result) -> boolish)?, ?unless: (^(untyped result) -> boolish)?, ?shared: bool, ?key: (^(*untyped args, **untyped kwargs) -> untyped)?) -> void
21
34
  def reset_shared_memo: (Symbol | String method_name, *untyped args, **untyped kwargs) -> void
22
35
  def reset_all_shared_memos: () -> void
23
36
  def shared_memoized?: (Symbol | String method_name, *untyped args, **untyped kwargs) -> bool
24
37
  def shared_memo_count: (?Symbol | String method_name) -> Integer
38
+ def shared_memo_age: (Symbol | String method_name, *untyped args, **untyped kwargs) -> Float?
39
+ def shared_memo_stale?: (Symbol | String method_name, *untyped args, **untyped kwargs) -> bool
25
40
 
26
41
  private
27
42
 
28
43
  def __safe_memo_shared_cache__: () -> Hash[memo_key, memo_record]
29
44
  def __safe_memo_shared_mutex__: () -> Mutex
30
45
  def __safe_memo_shared_lru_order__: () -> Hash[Symbol, Hash[memo_key, true]]
46
+ def __safe_memo_class_key_generators__: () -> Hash[Symbol, Proc]
31
47
  def memoized_method_visibility: (Symbol method_name) -> Symbol
32
48
  end
33
49
 
@@ -43,12 +59,19 @@ module SafeMemoize
43
59
  def on_memo_evict: { (memo_key cache_key, memo_record record) -> untyped } -> void
44
60
  def on_memo_hit: { (memo_key cache_key, memo_record record) -> untyped } -> void
45
61
  def on_memo_miss: { (memo_key cache_key, memo_record record) -> untyped } -> void
62
+ def on_memo_store: { (memo_key cache_key, memo_record record) -> untyped } -> void
46
63
  def clear_memo_hooks: (Symbol? hook_type) -> void
47
64
  def warm_memo: (Symbol | String method_name, *untyped args, ?ttl: Numeric?, **untyped kwargs) { () -> untyped } -> untyped
65
+ def memo_preload: (Symbol | String method_name, *Array[untyped] arg_sets) -> Array[untyped]
48
66
  def dump_memo: (?Symbol | String method_name) -> Hash[memo_key, untyped]
49
67
  def load_memo: (Hash[memo_key, untyped] snapshot) -> nil
68
+ def memo_touch: (Symbol | String method_name, *untyped args, ?ttl: Numeric?, **untyped kwargs) -> bool
69
+ def memo_refresh: (Symbol | String method_name, *untyped args, **untyped kwargs) -> untyped
70
+ def memo_age: (Symbol | String method_name, *untyped args, **untyped kwargs) -> Float?
71
+ def memo_stale?: (Symbol | String method_name, *untyped args, **untyped kwargs) -> bool
50
72
  def reset_memo: (Symbol | String method_name, *untyped args, **untyped kwargs) -> void
51
73
  def reset_all_memos: () -> void
74
+ def memo_inspect: (Symbol | String method_name, *untyped args, **untyped kwargs) -> { cached: bool, value: untyped, hits: Integer, misses: Integer, ttl_remaining: Float?, age: Float?, custom_key: untyped, lru_position: Integer? }?
52
75
  end
53
76
 
54
77
  module CacheStoreMethods
@@ -89,14 +112,15 @@ module SafeMemoize
89
112
  def safe_memo_values_for: (Symbol? method_name) -> Array[untyped]
90
113
  def memo_projection: (memo_key cache_key, memo_record value, include_method: bool, include_value: bool) -> Hash[Symbol, untyped]
91
114
  def safe_memo_cache_key: (Symbol | String method_name, Array[untyped] args, Hash[Symbol, untyped] kwargs) -> default_memo_key
115
+ def deep_freeze_copy: (untyped obj) -> untyped
92
116
  end
93
117
 
94
118
  module HooksMethods
95
- @__safe_memo_hooks__: { on_expire: Array[Proc], on_evict: Array[Proc], on_hit: Array[Proc], on_miss: Array[Proc] }?
119
+ @__safe_memo_hooks__: { on_expire: Array[Proc], on_evict: Array[Proc], on_hit: Array[Proc], on_miss: Array[Proc], on_store: Array[Proc] }?
96
120
 
97
121
  private
98
122
 
99
- def memo_hook_store: () -> { on_expire: Array[Proc], on_evict: Array[Proc], on_hit: Array[Proc], on_miss: Array[Proc] }
123
+ def memo_hook_store: () -> { on_expire: Array[Proc], on_evict: Array[Proc], on_hit: Array[Proc], on_miss: Array[Proc], on_store: Array[Proc] }
100
124
  def register_memo_hook: (Symbol hook_type) { (memo_key cache_key, memo_record record) -> untyped } -> void
101
125
  def call_memo_hooks: (Symbol hook_type, memo_key cache_key, memo_record record) -> void
102
126
  def _clear_memo_hooks: (Symbol? hook_type) -> void
@@ -118,7 +142,7 @@ module SafeMemoize
118
142
  def cache_stats_for: (Symbol | String method_name) -> Hash[Symbol, untyped]
119
143
  def cache_hit_rate: () -> Float
120
144
  def cache_miss_rate: () -> Float
121
- def cache_metrics_reset: () -> void
145
+ def cache_metrics_reset: (?Symbol | String method_name) -> void
122
146
 
123
147
  private
124
148
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: safe_memoize
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.3
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chuck Smith
@@ -34,7 +34,7 @@ description: 'SafeMemoize is a production-ready, zero-dependency memoization lib
34
34
  cache key generators, and introspection helpers. Method visibility (public, protected,
35
35
  private) is fully preserved.'
36
36
  email:
37
- - eclectic-coding@users.noreply.github.com
37
+ - chuck@eclecticcoding.com
38
38
  executables: []
39
39
  extensions: []
40
40
  extra_rdoc_files: []
@@ -44,12 +44,14 @@ files:
44
44
  - CHANGELOG.md
45
45
  - LICENSE.txt
46
46
  - README.md
47
+ - ROADMAP.md
47
48
  - Rakefile
48
49
  - lib/safe_memoize.rb
49
50
  - lib/safe_memoize/cache_metrics_methods.rb
50
51
  - lib/safe_memoize/cache_record_methods.rb
51
52
  - lib/safe_memoize/cache_store_methods.rb
52
53
  - lib/safe_memoize/class_methods.rb
54
+ - lib/safe_memoize/configuration.rb
53
55
  - lib/safe_memoize/custom_key_methods.rb
54
56
  - lib/safe_memoize/hooks_methods.rb
55
57
  - lib/safe_memoize/inspection_methods.rb