Skip to content

v1.3.0

Choose a tag to compare

@github-actions github-actions released this 28 May 20:57
· 28 commits to main since this release

SafeMemoize 1.3.0

Added

  • 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.

    • 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"}).
    • 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.
    • Duck-type compatible — any object responding to handled_options, process_memoize_option, and dispatch_cache_event works without extend SafeMemoize::Extension.
  • SafeMemoize.register_extension(name, extension) — registers an extension under a symbolic name.

  • SafeMemoize.unregister_extension(name) — removes an extension.

  • SafeMemoize.extensions — returns a snapshot of the registry.

  • SafeMemoize.reset_extensions! — clears the registry (test teardown).

  • SafeMemoize.extension_for_option(option_name) — returns the registered extension that handles the named option, or nil.

  • 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.

  • 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:.

  • 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:.

  • SafeMemoize.shared_cache(name) — returns the Stores::Base instance for the given name, creating a new Stores::Memory if none is registered.

  • 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.

  • SafeMemoize.clear_shared_cache(name) — calls clear on the named store, evicting all entries. No-op for unregistered names.

  • SafeMemoize.drop_shared_cache(name) — removes the named store from the registry; subsequent shared_cache(name) calls will auto-create a new Memory store.

  • SafeMemoize.shared_caches — returns a dup of the current registry as a Hash{String => Stores::Base}.

  • SafeMemoize.reset_shared_caches! — clears the entire registry; useful in test-suite after hooks to prevent state leaking between examples.

  • 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.

  • .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.

  • 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!.

  • Resolution priority: per-method namespace: > class .safe_memoize_namespace > global Configuration#namespace.

  • 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.

  • 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.