Skip to content

Add safe_memoize_options and copy_on_read: true (v1.4.0)#59

Merged
eclectic-coding merged 2 commits into
mainfrom
feature/v1.4.0
Jun 2, 2026
Merged

Add safe_memoize_options and copy_on_read: true (v1.4.0)#59
eclectic-coding merged 2 commits into
mainfrom
feature/v1.4.0

Conversation

@eclectic-coding
Copy link
Copy Markdown
Owner

Summary

  • safe_memoize_options(**opts) — class-level macro that sets default options for every memoize call on the class. Per-call options take precedence; class defaults take precedence over global SafeMemoize.configure defaults. Accepts all memoize options except mode-switch options (shared:, fiber_local:, ractor_safe:, shared_cache:), which must be specified per call.
  • copy_on_read: true on memoize — returns a dup (or deep_dup when available) of the cached value on every read, preventing callers from mutating shared cached state. Works across all cache paths (per-instance, LRU, shared, fiber-local, external store). Incompatible with ractor_safe: (ractor values are always frozen). Can be set as a class-wide default via safe_memoize_options.
  • Both features are the two planned items for the v1.4.0 — Developer Experience milestone.

Implementation notes

  • Added UNSET = Object.new.freeze sentinel so every memoize keyword arg can distinguish "not passed" from explicitly nil/false, enabling clean three-tier precedence: per-call → class default → global config.
  • dup_fn is a lambda closed over by every define_method block; applied at all return points in every cache path with zero overhead when copy_on_read: false (the identity lambda).
  • Removed an unreachable rescue TypeError in dup_fn — all types that would raise are frozen in Ruby ≥ 3.3, so v.frozen? catches them first. Result: 100% line coverage.

Test plan

  • bundle exec rake — 811 examples, 0 failures, lint clean, 100% coverage
  • safe_memoize_options: TTL default, max_size default, copy_on_read default, global config fallback, clearing defaults, disallowed mode-switch options
  • copy_on_read:: object identity differs on every hit, cache protected from mutation, nil/frozen pass-through, max_size: locked path, shared: path, ttl: path, deep_dup delegation, ractor_safe: incompatibility guard

🤖 Generated with Claude Code

eclectic-coding and others added 2 commits June 2, 2026 11:39
- UNSET sentinel on all memoize keyword args so class defaults can
  distinguish "not passed" from explicitly nil/false
- safe_memoize_options(**opts) class macro: sets defaults for ttl:,
  max_size:, ttl_refresh:, if:, unless:, key:, cache_bust:,
  copy_on_read:, namespace:, and store:; per-call options override;
  raises ArgumentError for mode-switch options (shared:, fiber_local:,
  ractor_safe:, shared_cache:)
- copy_on_read: true on memoize: returns dup / deep_dup of the cached
  value on every read across all paths (fast, locked, shared, fiber-
  local, external store); nil and frozen values pass through unchanged;
  incompatible with ractor_safe: (values are always frozen there)
- 23 new examples; 100% line coverage

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- CHANGELOG: add Unreleased entries for both features
- ROADMAP: mark both v1.4.0 items as Shipped
- README: feature list entries, two new sections with examples,
  options reference table rows, safe_memoize_options API table

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@eclectic-coding eclectic-coding merged commit b5fd321 into main Jun 2, 2026
4 checks passed
@eclectic-coding eclectic-coding deleted the feature/v1.4.0 branch June 2, 2026 15:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant