safe_memoize 1.1.0 → 1.3.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: 12783636e7ebfd6b21453d9262eb81157d9c82de3a9a538fb8825fd173f072f6
4
- data.tar.gz: fff5a502ff49712cf365b3fc67d337ad279f1b28e7ed604ee63b8dcf43e0f6bd
3
+ metadata.gz: 3e47bd422b1516a4f775e49ad3f58bf916df60c094aae7dfddf652fa78e44d73
4
+ data.tar.gz: 2adc318d5cda8858d1ee49a2dd32080acea1e8163807e424366d9e8e42a30943
5
5
  SHA512:
6
- metadata.gz: 0eedea81071d36fb8891c8767b43b07e5873aaca7d0f14bafdc339acf9c0c040b1e9b050ff72ec840d0edeccf33ef422082423576b5ae58e371dec13f50e4ed0
7
- data.tar.gz: 506cadf95e962b1dccf167be9566663ee44d9d4de8ee7eed75bad4189596674b0dc6e485b9063a37f222b1136698b28f031f56d2a035f04e6d3ffa13fcc34b50
6
+ metadata.gz: de347fc9b0e6f9ad1385f0f5f3a1cc03595f9cf8b969f354cd87bd5173e318ec9af98cf31d2253617c369a8cd7ea9d6c08fbc489d22d30775b912f6867bab70f
7
+ data.tar.gz: '08153a2284f53e6110d4f3fae0a84ae72d2739bc0146e70cbabd5a6cce30b544122204623185ad96c1706c1de518feb583a227fd8b74018b896775f884ac7053'
@@ -53,12 +53,12 @@ jobs:
53
53
  bundler-cache: true
54
54
 
55
55
  - name: Run test suite
56
- run: bundle exec rspec
56
+ run: bundle exec rake spec
57
57
 
58
58
  - name: Upload coverage to Codecov
59
59
  uses: codecov/codecov-action@v5
60
60
  if: matrix.ruby == '3.4'
61
61
  with:
62
62
  token: ${{ secrets.CODECOV_TOKEN }}
63
- files: coverage/.resultset.json
63
+ files: coverage/coverage.json
64
64
 
data/CHANGELOG.md CHANGED
@@ -8,6 +8,62 @@ from v1.0.0 onwards. Prior 0.x releases may include breaking changes between min
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [1.3.0] - 2026-05-28
12
+
13
+ ### Added
14
+
15
+ - `SafeMemoize::Extension` — mixin for building SafeMemoize extensions. Extend it in any module to get a DSL for declaring custom `memoize` options and global lifecycle event handlers without monkey-patching SafeMemoize internals.
16
+ - `handles_option(name, &processor)` — declares a custom keyword argument that `memoize` will accept; the processor block is called at definition time with `(value, method_name, all_extension_options)` and must return a `Hash` of standard memoize options to inject (e.g. `{cache_bust: ...}`, `{ttl: 60}`, `{namespace: "v2"}`).
17
+ - `on_cache_event(*event_types, &handler)` — registers a global lifecycle handler that fires after every matching event (`:on_hit`, `:on_miss`, `:on_store`, `:on_expire`, `:on_evict`) across all memoized methods on all classes; handler receives `(klass, method_name, cache_key, record)`; runs on the main Ractor only.
18
+ - Duck-type compatible — any object responding to `handled_options`, `process_memoize_option`, and `dispatch_cache_event` works without `extend SafeMemoize::Extension`.
19
+ - `SafeMemoize.register_extension(name, extension)` — registers an extension under a symbolic name.
20
+ - `SafeMemoize.unregister_extension(name)` — removes an extension.
21
+ - `SafeMemoize.extensions` — returns a snapshot of the registry.
22
+ - `SafeMemoize.reset_extensions!` — clears the registry (test teardown).
23
+ - `SafeMemoize.extension_for_option(option_name)` — returns the registered extension that handles the named option, or `nil`.
24
+ - `memoize` now accepts `**extension_options` for any unknown keyword argument; each key is validated against registered extensions at call time and raises `ArgumentError` if no extension claims it, preserving the existing strict-options behaviour for typos.
25
+
26
+ - `cache_bust: callable` option on `memoize` — automatic cache invalidation driven by a version token. A callable (Proc, lambda, or Symbol naming an instance method) is invoked on the instance at every cache lookup; the returned token is folded into the cache key alongside the normal arguments. When the token changes (e.g. an ActiveRecord `updated_at` advances after a `save`), the old key no longer matches any entry — the method body is recomputed and stored under the new key without any explicit `reset_memo` call. Accepts a zero-argument callable invoked via `instance_exec` (giving access to `self`, instance variables, and methods) or a `Symbol` naming an instance method. Returns any comparable value as the token: a `Time`, `Integer`, `String`, `Array`, etc. Old token entries accumulate as stale; pair with `ttl:` or a store adapter's eviction to bound memory. Incompatible with `key:`. Composes with `namespace:`, `ttl:`, `if:`, `unless:`, and `shared_cache:`.
27
+
28
+ - `shared_cache: "name"` option on `memoize` — routes all reads and writes through a globally-registered named `Stores::Base` instance, enabling cross-class cache sharing. Any number of unrelated classes can share the same backing store by referencing the same name. The store is resolved at `memoize` definition time via `SafeMemoize.shared_cache("name")`, which auto-creates a `Stores::Memory` instance on first access; supply a custom adapter (Redis, RailsCache, etc.) by calling `SafeMemoize.register_shared_cache("name", store)` before any class that references the name is loaded. Incompatible with `shared:`, `store:`, `fiber_local:`, `ractor_safe:`, and `max_size:`; composes naturally with `namespace:`, `ttl:`, `if:`, `unless:`, and `key:`.
29
+ - `SafeMemoize.shared_cache(name)` — returns the `Stores::Base` instance for the given name, creating a new `Stores::Memory` if none is registered.
30
+ - `SafeMemoize.register_shared_cache(name, store)` — registers a custom `Stores::Base` instance under a name; must be called before any class that uses that name via `shared_cache:` is loaded.
31
+ - `SafeMemoize.clear_shared_cache(name)` — calls `clear` on the named store, evicting all entries. No-op for unregistered names.
32
+ - `SafeMemoize.drop_shared_cache(name)` — removes the named store from the registry; subsequent `shared_cache(name)` calls will auto-create a new `Memory` store.
33
+ - `SafeMemoize.shared_caches` — returns a dup of the current registry as a `Hash{String => Stores::Base}`.
34
+ - `SafeMemoize.reset_shared_caches!` — clears the entire registry; useful in test-suite `after` hooks to prevent state leaking between examples.
35
+
36
+ - `namespace:` option on `memoize` — a String prefix scoped to a single method; prepended to the cache key's first element so that entries with different namespaces never collide, even when sharing the same store or the same per-instance hash. Must be a non-empty string without `:`. Useful for versioning one method independently of its peers.
37
+ - `.safe_memoize_namespace` / `.safe_memoize_namespace=` — class-level namespace attribute; applies to every `memoize` call on the class that does not specify its own `namespace:` option. Takes precedence over the global `SafeMemoize::Configuration#namespace`.
38
+ - `SafeMemoize::Configuration#namespace` — global namespace prefix applied to every `memoize` call site that has no per-method or class-level namespace set. Set via `SafeMemoize.configure { |c| c.namespace = "v1" }`. Useful for versioned deployments and multi-tenant setups. Cleared by `reset_configuration!`.
39
+ - Resolution priority: per-method `namespace:` > class `.safe_memoize_namespace` > global `Configuration#namespace`.
40
+ - All introspection methods (`memoized?`, `memo_count`, `memo_keys`, `memo_values`, `reset_memo`, `reset_all_memos`, `dump_memo`, `cache_stats_for`, `cache_metrics_reset`, shared-cache equivalents, etc.) accept the bare method name regardless of which namespace tier is active; the `:method` field in projections always returns the bare method name.
41
+ - Ractor-safe: namespace resolution uses `instance_variable_get` (read-only) so worker Ractors can call `compute_cache_key` without triggering unshareable class-level ivar initialization.
42
+
43
+ ## [1.2.0] - 2026-05-27
44
+
45
+ ### Added
46
+
47
+ - `SafeMemoize::Adapters::ConcurrentRuby` — optional store adapter backed by `concurrent-ruby`; uses `Concurrent::Map` as the backing hash and `Concurrent::ReentrantReadWriteLock` to allow multiple readers to proceed in parallel while writers still get exclusive access; reduces lock contention on hot read paths compared to the default `Mutex`-backed `Stores::Memory`; `concurrent-ruby` is a soft dependency — a `LoadError` with an actionable message is raised at instantiation if the gem is not available
48
+ - `.safe_memoize_store=` / `.safe_memoize_store` — class-level attribute for setting a default store on an individual class without touching the global `SafeMemoize.configure` default; takes precedence over `SafeMemoize.configuration.default_store` but is overridden by a per-method `store:` argument; accepts any `SafeMemoize::Stores::Base` instance or `nil`; raises `ArgumentError` for invalid values
49
+
50
+ - `ractor_safe: true` option on `memoize` — replaces the `Mutex`-backed class-level shared cache with a supervisor `Ractor` that owns the mutable cache hash; all reads and writes are serialised through message passing so the cache is safe to use from multiple Ractors; requires `shared: true`; cached values are deep-frozen via `Ractor.make_shareable`; the memoize wrapper Proc is frozen with `Ractor.make_shareable` before being passed to `define_method` so that classes using `ractor_safe: true` can be passed directly into worker Ractors; incompatible with `if:`, `unless:`, `max_size:`, `ttl_refresh:`, `key:`, and `store:` (raises `ArgumentError`); `ttl:` is supported
51
+ - `.reset_ractor_memo(method_name, *args, **kwargs)` — class method to clear one or all entries from the Ractor-safe shared cache for a given method
52
+ - `.reset_all_ractor_memos` — class method to clear the entire Ractor-safe shared cache for this class
53
+ - `.ractor_memoized?(method_name, *args, **kwargs)` — returns `true` if a live entry exists in the Ractor-safe shared cache for the given call signature
54
+ - `.ractor_memo_count(method_name = nil)` — returns the number of live entries in the Ractor-safe shared cache; scoped to one method when a name is given
55
+ - `fiber_local: true` option on `memoize` — stores results in `Fiber[:__safe_memoize__]` rather than instance variables, giving each fiber its own isolated cache that is automatically discarded when the fiber terminates; no `Mutex` is acquired because fibers are cooperative; a per-fiber ownership sentinel ensures inherited storage from parent fibers is replaced with a fresh isolated store on first write; supports all standard options (`ttl:`, `ttl_refresh:`, `max_size:`, `if:`, `unless:`, `key:`); incompatible with `shared:` and `store:` (raises `ArgumentError`)
56
+ - `#fiber_local_memoized?(method_name, *args, **kwargs)` — returns `true` if the given call is currently cached in the current fiber's store
57
+ - `#reset_fiber_memo(method_name, *args, **kwargs)` — clears one or all fiber-local cached entries for a method in the current fiber
58
+ - `#reset_all_fiber_memos` — clears all fiber-local cached entries for this instance in the current fiber
59
+
60
+ ### Fixed
61
+
62
+ - `call_memo_hooks` no longer raises `Ractor::IsolationError` when called from a worker Ractor — `SafeMemoize.configuration` (a module-level ivar) is now accessed only from the main Ractor; `ActiveSupport::Notifications` and `StatsD` dispatch are silently skipped from worker Ractors; hook-error handling falls back to `warn` rather than reading the configuration handler
63
+ - CI coverage ordering — ractor specs now run *last* (after all other specs) so that Ractor background threads cannot disrupt Ruby's Coverage counters while collecting coverage for non-Ractor code; previously only store specs were ordered first
64
+ - Codecov reporting accuracy — switched SimpleCov output from `.resultset.json` (internal format, misread by Codecov as ~85%) to `coverage/coverage.json` via `simplecov_json_formatter`; CI now uploads the correct file
65
+ - CI coverage ordering — `bundle exec rspec` ran files alphabetically, causing `ractor_spec.rb` to execute before `spec/stores/`, disrupting Ruby's Coverage counters and dropping reported coverage to ~96%; CI now uses `bundle exec rake spec`, which enforces the store-first ordering already documented in the Rakefile
66
+
11
67
  ## [1.1.0] - 2026-05-22
12
68
 
13
69
  ### Added
@@ -25,7 +81,7 @@ from v1.0.0 onwards. Prior 0.x releases may include breaking changes between min
25
81
  - `store:` type guard in `ClassMethods#memoize` collapsed to an inline guard clause so Ruby's Coverage module counts the raise correctly
26
82
  - Hook-error isolation tests (`concurrency_spec`, `hooks_spec`) now configure `on_hook_error = ->(*) {}` to silence expected stderr warnings rather than leaking them into test output; StatsD error-resilience test asserts on the emitted warning with `expect { }.to output(...).to_stderr`
27
83
 
28
- ## [1.0.0] - 2026-05-22
84
+ ## [1.0.0] - 2026-05-22
29
85
 
30
86
  ### Added
31
87