| File: | blib/lib/Class/Simple/Readonly/Cached.pm |
| Coverage: | 98.2% |
| line | stmt | bran | cond | sub | time | code |
|---|---|---|---|---|---|---|
| 1 | package Class::Simple::Readonly::Cached; | |||||
| 2 | ||||||
| 3 | 13 13 13 | 1470359 31 182 | use strict; | |||
| 4 | 13 13 13 | 19 11 213 | use warnings; | |||
| 5 | ||||||
| 6 | 13 13 13 | 23 11 308 | use Carp; | |||
| 7 | 13 13 13 | 28 17 326 | use List::Util qw(none); | |||
| 8 | 13 13 13 | 26 13 193 | use Scalar::Util qw(blessed); | |||
| 9 | 13 13 13 | 2103 22578 164 | use Class::Simple; | |||
| 10 | 13 13 13 | 2613 66123 372 | use Data::Reuse; | |||
| 11 | 13 13 13 | 1395 56763 256 | use Params::Get 0.15; | |||
| 12 | 13 13 13 | 28 11 318 | use Readonly; | |||
| 13 | ||||||
| 14 | # Private-sub enforcement: _name subs croak if called from outside this | |||||
| 15 | # package. Sub::Private respects $ENV{HARNESS_ACTIVE}, so white-box tests | |||||
| 16 | # (run under prove / make test) are automatically exempt. | |||||
| 17 | 13 | 132 | BEGIN { $Sub::Private::config{mode} = 'enforce' } | |||
| 18 | 13 13 13 | 2633 261590 35 | use Sub::Private 0.05; | |||
| 19 | ||||||
| 20 | # @ISA is intentionally EMPTY. | |||||
| 21 | # | |||||
| 22 | # Class::Simple creates permanent named methods in its own namespace the | |||||
| 23 | # first time any accessor is called (e.g. the first $obj->val() call | |||||
| 24 | # installs a real Class::Simple::val sub). If we set @ISA = ('Class::Simple'), | |||||
| 25 | # those installed methods would be found by Perl's normal method lookup on | |||||
| 26 | # CSRC objects and would bypass our AUTOLOAD -- breaking the cache entirely. | |||||
| 27 | # | |||||
| 28 | # Keeping @ISA empty ensures every method call on a CSRC object is handled | |||||
| 29 | # by our AUTOLOAD. The isa() and can() overrides below restore correct | |||||
| 30 | # UNIVERSAL behaviour for isa-of-Class::Simple checks. | |||||
| 31 | our @ISA = (); | |||||
| 32 | ||||||
| 33 | # Package-level registry mapping inner-object refs (stringified) to a | |||||
| 34 | # record of { object => $wrapper, file => ..., line => ... }. | |||||
| 35 | # Used to detect and warn about double-wrapping. | |||||
| 36 | our %cached; | |||||
| 37 | ||||||
| 38 | # Stored in the cache wherever a method returned undef or an empty list, | |||||
| 39 | # so we can distinguish "not yet cached" from "cached-undef". | |||||
| 40 | Readonly::Scalar my $UNDEF_SENTINEL => __PACKAGE__ . '>UNDEF<'; | |||||
| 41 | ||||||
| 42 | # CHI expiry value meaning "never expire this entry". | |||||
| 43 | Readonly::Scalar my $CHI_NEVER => 'never'; | |||||
| 44 | ||||||
| 45 - 53 | =head1 NAME Class::Simple::Readonly::Cached - cache messages to an object =head1 VERSION Version 0.13 =cut | |||||
| 54 | ||||||
| 55 | our $VERSION = '0.13'; | |||||
| 56 | ||||||
| 57 - 193 | =head1 SYNOPSIS
A caching decorator for L<Class::Simple>-based (and arbitrary) objects.
It is up to the caller to maintain the cache if the object comes out of
sync with the cache, for example by changing its state.
use Class::Simple::Readonly::Cached;
my $obj = Class::Simple->new();
$obj->val('foo');
my $cached = Class::Simple::Readonly::Cached->new(
object => $obj,
cache => {},
);
my $val = $cached->val(); # calls the real object
my $val2 = $cached->val(); # served from cache
$val = $cached->val(a => 'b'); # args form part of the cache key
Note that when the object goes out of scope (DESTROY is called), the
cache is cleared automatically.
=head1 DESCRIPTION
Wraps any Perl object in a transparent caching layer. Every method call
is intercepted via AUTOLOAD; on the first call (a I<miss>) the result is
stored in the cache and returned. Subsequent identical calls (same method
name, same argument list) are I<hits> and are served directly from the
cache without touching the inner object.
Two cache backends are supported: a plain hash reference (fast, in-process,
no expiry) and any CHI-compatible object (persistent, shared, with expiry).
=head1 SUBROUTINES/METHODS
=head2 new
Construct a caching proxy around any Perl object.
=head3 Arguments
=over 4
=item C<cache> (mandatory)
Either a plain hash reference (C<{}>) or a CHI-compatible object that
implements C<get()>, C<set()>, and C<purge()>.
=item C<object> (optional)
The object to wrap. Defaults to a bare L<Class::Simple> instance.
Must be a reference; a plain scalar argument causes a C<carp> and an
C<undef> return. Wrapping an already-wrapped
C<Class::Simple::Readonly::Cached> object returns the existing wrapper
with a warning.
=item C<quiet> (optional, boolean)
Suppress the double-wrap warning when non-zero.
=back
=head3 Returns
A C<Class::Simple::Readonly::Cached> object, or C<undef> on invalid
C<object>. Croaks on invalid C<cache>.
=head3 EXAMPLE
use CHI;
use Class::Simple::Readonly::Cached;
# --- Hash-ref cache (in-process, no expiry) ---
my $obj = My::Expensive->new();
my $cached = Class::Simple::Readonly::Cached->new(
object => $obj,
cache => {},
);
my $result = $cached->compute(); # calls the real object
my $result2 = $cached->compute(); # from cache -- object not called
# --- CHI cache (persistent, file-based) ---
use File::Temp qw(tempdir);
my $chi = CHI->new(driver => 'File', root_dir => tempdir(CLEANUP => 1));
my $cached2 = Class::Simple::Readonly::Cached->new(
object => $obj,
cache => $chi,
);
# --- Clone an existing wrapper ---
my $clone = $cached->new(); # shares the same inner object and cache
=head3 API SPECIFICATION
# Input
{
cache => { type => ['hashref', 'object'], required => 1 },
object => { type => 'ref', optional => 1 },
quiet => { type => 'bool', optional => 1 },
}
# Output
{ type => 'object', class => 'Class::Simple::Readonly::Cached',
optional => 1 }
=head3 MESSAGES
Message Meaning Resolution
------- ------- ----------
Cache must be ref to HASH or object cache is not a hashref or blessed Pass \%hash or a CHI object.
object
Cache object must implement get(), set(), and purge() blessed cache lacks required API Use a CHI-compatible object.
$object must be a reference, not a scalar object is a plain string Pass a blessed reference.
warning: $object is already a cached object wrapping an already-wrapped object Reuse the returned wrapper.
$object is already cached at LINE of FILE double-wrap detected Reuse the existing wrapper;
set quiet => 1 to silence.
=head3 PSEUDOCODE
1. If class is undef: carp and return undef (::new() misuse)
2. If class is blessed: merge params into a clone and return
3. Validate cache: croak if not a hashref or CHI-compatible object
4. Validate object: carp+return if scalar; return existing
wrapper if already __PACKAGE__
5. Create inner object: Class::Simple->new(non-wrapper params)
unless object was supplied
6. Check double-wrap registry: if object in %cached, carp and return
existing wrapper (unless quiet)
7. Bless and register: bless $params, $class; set _class = $class;
call _build_cache_accessors to install
_get/_set coderefs and _cache_is_hash;
store in %cached with caller file and line
8. Return $self
=cut | |||||
| 194 | ||||||
| 195 | sub new | |||||
| 196 | { | |||||
| 197 | 177 | 1024551 | my $class = shift; | |||
| 198 | ||||||
| 199 | # Guard against the common mistake of calling ::new() instead of ->new(). | |||||
| 200 | 177 | 219 | if(!defined($class)) { | |||
| 201 | 1 | 9 | Carp::carp(__PACKAGE__ . ': use ->new() not ::new() to instantiate'); | |||
| 202 | 1 | 104 | return; | |||
| 203 | } | |||||
| 204 | ||||||
| 205 | # Object invocation: clone the existing wrapper, merging any new params. | |||||
| 206 | 176 | 233 | if(blessed($class)) { | |||
| 207 | 9 | 26 | my $params = Params::Get::get_params(undef, \@_) // {}; | |||
| 208 | 9 9 9 | 110 11 19 | my $clone = bless { %{$class}, %{$params} }, ref($class); | |||
| 209 | # If the caller overrides the cache, rebuild the pre-computed dispatch | |||||
| 210 | # closures so they target the new backend, not the original one. | |||||
| 211 | 9 | 15 | if(exists $params->{cache}) { | |||
| 212 | 3 | 5 | _build_cache_accessors($clone); | |||
| 213 | } | |||||
| 214 | 9 | 16 | return $clone; | |||
| 215 | } | |||||
| 216 | ||||||
| 217 | 167 | 234 | my $params = Params::Get::get_params('cache', @_); | |||
| 218 | ||||||
| 219 | # Validate the cache argument before doing anything else. | |||||
| 220 | 166 | 2112 | if(blessed($params->{cache})) { | |||
| 221 | 18 | 115 | unless($params->{cache}->can('get') | |||
| 222 | && $params->{cache}->can('set') | |||||
| 223 | && $params->{cache}->can('purge')) | |||||
| 224 | { | |||||
| 225 | 3 | 12 | Carp::croak("$class: Cache object must implement get(), set(), and purge()"); | |||
| 226 | } | |||||
| 227 | } elsif(ref($params->{cache}) ne 'HASH') { | |||||
| 228 | 9 | 39 | Carp::croak("$class: Cache must be ref to HASH or object"); | |||
| 229 | } | |||||
| 230 | ||||||
| 231 | 156 | 525 | if(defined($params->{object})) { | |||
| 232 | 149 | 146 | if(!ref($params->{object})) { | |||
| 233 | 3 | 11 | Carp::carp(__PACKAGE__ . ': $object must be a reference, not a scalar'); | |||
| 234 | 3 | 227 | return; | |||
| 235 | } | |||||
| 236 | 146 | 170 | if(ref($params->{object}) eq __PACKAGE__) { | |||
| 237 | # Silently returning the existing wrapper is safer than building | |||||
| 238 | # a second layer that would double-count misses/hits. | |||||
| 239 | 3 | 13 | Carp::carp(__PACKAGE__ . ': warning: $object is already a cached object'); | |||
| 240 | 3 | 132 | return $params->{object}; | |||
| 241 | } | |||||
| 242 | } else { | |||||
| 243 | # No inner object supplied: create a bare Class::Simple instance. | |||||
| 244 | # Forward only non-wrapper keys so that 'cache' and 'quiet' do not | |||||
| 245 | # bleed into the inner object's attribute hash. | |||||
| 246 | 7 7 | 5 11 | my %inner = %{$params}; | |||
| 247 | 7 | 8 | delete @inner{qw(cache quiet)}; # O(1) hash-slice delete, no temp list | |||
| 248 | 7 | 19 | $params->{object} = Class::Simple->new(%inner); | |||
| 249 | } | |||||
| 250 | ||||||
| 251 | # Warn if this inner object is already wrapped in a cache -- returning the | |||||
| 252 | # existing wrapper prevents hidden double-caching with stale hit counts. | |||||
| 253 | 150 | 855 | if(my $existing = $cached{$params->{object}}) { | |||
| 254 | 7 | 13 | unless($params->{quiet}) { | |||
| 255 | Carp::carp(__PACKAGE__ . ' $object is already cached at ' | |||||
| 256 | 4 | 21 | . $existing->{line} . ' of ' . $existing->{file}); | |||
| 257 | } | |||||
| 258 | 7 | 702 | return $existing->{object}; | |||
| 259 | } | |||||
| 260 | ||||||
| 261 | 143 | 134 | my $self = bless $params, $class; | |||
| 262 | ||||||
| 263 | # Pre-compute values used on every AUTOLOAD dispatch. | |||||
| 264 | 143 | 155 | $self->{_class} = $class; | |||
| 265 | 143 | 191 | _build_cache_accessors($self); | |||
| 266 | ||||||
| 267 | 143 | 195 | my (undef, $file, $line) = caller(0); # destructure: only fields 1 and 2 needed | |||
| 268 | $cached{$self->{object}} = { | |||||
| 269 | 143 | 1246 | object => $self, | |||
| 270 | file => $file, | |||||
| 271 | line => $line, | |||||
| 272 | }; | |||||
| 273 | ||||||
| 274 | 143 | 181 | return $self; | |||
| 275 | } | |||||
| 276 | ||||||
| 277 - 299 | =head2 object
Return the inner (wrapped) object.
=head3 Returns
The blessed reference that was passed as C<object> to C<new()>.
=head3 EXAMPLE
# Bypass the cache to mutate state directly.
$cached->object()->reset();
=head3 API SPECIFICATION
# Input none
# Output { type => 'object' }
=head3 MESSAGES
(none)
=cut | |||||
| 300 | ||||||
| 301 | sub object | |||||
| 302 | { | |||||
| 303 | 15 | 1611 | return $_[0]->{object}; | |||
| 304 | } | |||||
| 305 | ||||||
| 306 - 347 | =head2 state
Return a snapshot of cache hit and miss counts per cache key.
Primarily useful for performance profiling and white-box tests.
=head3 Returns
A hash reference:
=over 4
=item C<hits>
Hash reference mapping each cache key to the number of times the
result was served from cache. C<undef> until the first hit.
=item C<misses>
Hash reference mapping each cache key to the number of times the
inner object was actually invoked. C<undef> until the first miss.
=back
=head3 EXAMPLE
my $s = $cached->state();
my $hits = do { my $n=0; $n += $_ for values %{$s->{hits} // {}}; $n };
my $misses = do { my $n=0; $n += $_ for values %{$s->{misses} // {}}; $n };
printf "Hit rate: %.0f%%\n", 100 * $hits / ($hits + $misses) if $hits + $misses;
=head3 API SPECIFICATION
# Input None
# Output { type => 'hashref',
# keys => { hits => 'hashref|undef',
# misses => 'hashref|undef' } }
=head3 MESSAGES
(none)
=cut | |||||
| 348 | ||||||
| 349 | sub state | |||||
| 350 | { | |||||
| 351 | 46 | 8342 | my $self = shift; | |||
| 352 | 46 | 107 | return { hits => $self->{_hits}, misses => $self->{_misses} }; | |||
| 353 | } | |||||
| 354 | ||||||
| 355 - 380 | =head2 can
Report whether the inner object (or this class) can respond to a
given method. Overrides C<UNIVERSAL::can> to account for the
decorator pattern.
=head3 Returns
A code reference if the method exists, C<undef> otherwise.
=head3 EXAMPLE
my $code = $cached->can('compute');
$code->($cached) if $code;
=head3 API SPECIFICATION
# Input { self => { type => 'object|string' },
# method => { type => 'string' } }
# Output { type => 'coderef|undef' }
=head3 MESSAGES
(none)
=cut | |||||
| 381 | ||||||
| 382 | sub can | |||||
| 383 | { | |||||
| 384 | 54 | 3817 | my ($self, $method) = @_; | |||
| 385 | ||||||
| 386 | # Premise: 'new' belongs to the wrapper, not to the inner object. | |||||
| 387 | # Conclusion: short-circuit before any object check is needed. | |||||
| 388 | 54 | 89 | return \&new if $method eq 'new'; | |||
| 389 | ||||||
| 390 | # Premise: if $self is not a ref (class-level call) OR the inner object | |||||
| 391 | # has been freed (global destruction), there is no object to delegate to. | |||||
| 392 | # Conclusion: fall back to UNIVERSAL for what this package directly provides. | |||||
| 393 | return $self->SUPER::can($method) | |||||
| 394 | 49 | 165 | if !ref($self) || !ref($self->{object}); | |||
| 395 | ||||||
| 396 | # Premise: $self->{object} is alive and is a valid reference (Invariant I4). | |||||
| 397 | # Conclusion: delegate to the inner object first; fall back to UNIVERSAL. | |||||
| 398 | 17 | 118 | return $self->{object}->can($method) // $self->SUPER::can($method); | |||
| 399 | } | |||||
| 400 | ||||||
| 401 - 425 | =head2 isa
Test class membership, delegating to the inner object's class
hierarchy when needed. Overrides C<UNIVERSAL::isa> to support the
transparent decorator pattern.
=head3 Returns
True if the wrapper or its inner object is-a C<$class>.
=head3 EXAMPLE
$cached->isa('My::Domain::Object'); # true if inner object is
=head3 API SPECIFICATION
# Input { self => { type => 'object|string' },
# class => { type => 'string' } }
# Output { type => 'bool' }
=head3 MESSAGES
(none)
=cut | |||||
| 426 | ||||||
| 427 | sub isa | |||||
| 428 | { | |||||
| 429 | 49 | 2267 | my ($self, $class) = @_; | |||
| 430 | ||||||
| 431 | # Fast-path guards that do not require examining the inner object. | |||||
| 432 | # | |||||
| 433 | # Premise 1: $class eq ref($self) is logically subsumed by SUPER::isa, | |||||
| 434 | # which also returns true for an exact class match -- but the string | |||||
| 435 | # equality check is O(1) and avoids the UNIVERSAL dispatch overhead. | |||||
| 436 | # Keeping it as a fast path is a valid micro-optimisation, NOT dead code. | |||||
| 437 | # Premise 2: $class eq __PACKAGE__ handles the subclass case: | |||||
| 438 | # a My::Cached instance asked isa('Class::Simple::Readonly::Cached'). | |||||
| 439 | # Premise 3: $class eq 'Class::Simple' is required because @ISA is | |||||
| 440 | # intentionally empty; SUPER::isa would return false without this. | |||||
| 441 | # Premise 4: SUPER::isa covers the full UNIVERSAL hierarchy. | |||||
| 442 | # Conclusion: any of these makes the wrapper itself a member of $class. | |||||
| 443 | 49 | 200 | return 1 if $class eq ref($self) | |||
| 444 | || $class eq __PACKAGE__ | |||||
| 445 | || $class eq 'Class::Simple' | |||||
| 446 | || $self->SUPER::isa($class); | |||||
| 447 | ||||||
| 448 | # Premise: none of the wrapper-level checks matched. | |||||
| 449 | # Premise: $self->{object} may be freed during global destruction; guard | |||||
| 450 | # with ref() before delegating. | |||||
| 451 | # Conclusion: isa is true iff the inner object claims it. | |||||
| 452 | 19 | 115 | return !!(ref($self) && ref($self->{object}) && $self->{object}->isa($class)); | |||
| 453 | } | |||||
| 454 | ||||||
| 455 | # _build_cache_accessors -- install backend-specific _get/_set closures on $self. | |||||
| 456 | # The cache backend (HASH vs CHI) is fixed for a wrapper's lifetime. | |||||
| 457 | # Deciding which branch to take on every AUTOLOAD call via ref($cache) | |||||
| 458 | # plus a Sub::Private caller() check is unnecessary overhead. Instead | |||||
| 459 | # we close over the bare cache reference once at construction time and | |||||
| 460 | # expose two named slots (_get/_set) that AUTOLOAD calls directly. | |||||
| 461 | # The closures capture the cache ref as a lexical -- no reference back | |||||
| 462 | # to $self, so no circular-reference hazard. | |||||
| 463 | # Entry: $self is a fully-blessed wrapper with {cache} already set. | |||||
| 464 | # Exit: $self->{_get}, $self->{_set}, and $self->{_cache_is_hash} are set. | |||||
| 465 | # Side-effects: Mutates $self. | |||||
| 466 | sub _build_cache_accessors :Private | |||||
| 467 | { | |||||
| 468 | 97 | 71 | my ($self) = @_; | |||
| 469 | 97 | 74 | my $c = $self->{cache}; # captured by the closures below | |||
| 470 | 97 | 89 | if(ref($c) eq 'HASH') { | |||
| 471 | 87 | 73 | $self->{_cache_is_hash} = 1; | |||
| 472 | 87 2102 | 121 1574 | $self->{_get} = sub { $c->{$_[0]} }; | |||
| 473 | 87 1059 | 116 852 | $self->{_set} = sub { $c->{$_[0]} = $_[1] }; | |||
| 474 | } else { | |||||
| 475 | 10 | 8 | $self->{_cache_is_hash} = 0; | |||
| 476 | 10 15 | 17 18 | $self->{_get} = sub { $c->get($_[0]) }; | |||
| 477 | 10 8 | 17 11 | $self->{_set} = sub { $c->set($_[0], $_[1], $CHI_NEVER) }; | |||
| 478 | } | |||||
| 479 | 13 13 13 | 7183 10 178 | } | |||
| 480 | ||||||
| 481 | # _can_fixate -- decide whether Data::Reuse::fixate is safe on a list. | |||||
| 482 | # fixate cannot handle GLOBs or blessed objects (RT#100461, | |||||
| 483 | # RT#163955). Only plain ARRAY, HASH, and SCALAR refs are safe. | |||||
| 484 | # Entry: @_ is the list of values returned by the wrapped method. | |||||
| 485 | # Exit: Returns 1 (safe) or 0 (unsafe). | |||||
| 486 | sub _can_fixate :Private | |||||
| 487 | { | |||||
| 488 | return none { | |||||
| 489 | 20 | 16 | my $r = ref($_); | |||
| 490 | 20 | 35 | $r && $r !~ /\A (?:ARRAY|HASH|SCALAR) \z/x # GLOBs and blessed refs are unsafe for fixate | |||
| 491 | 10 | 25 | } @_; | |||
| 492 | 13 13 13 | 1623 25 119 | } | |||
| 493 | ||||||
| 494 - 535 | =head2 AUTOLOAD
Not called directly. Intercepts every method call not explicitly
defined in this package, looks up the result in the cache, and on a
miss proxies the call to the inner object and stores the result.
Cache lookup and storage use the pre-built C<_get>/C<_set> coderefs
installed by C<_build_cache_accessors> at construction time, so the
backend-type decision (HASH vs CHI) is made once -- never on each
dispatch.
Three stored-value forms are mutually exclusive and exhaustive:
=over 4
=item ARRAY ref
The wrapped method previously returned a list. Served as C<@array>
in list context, or C<$array[-1]> in scalar context.
=item C<$UNDEF_SENTINEL>
The wrapped method returned C<undef> or an empty list. Stored as the
sentinel string so a cache miss (undefined value) can be distinguished
from a cached C<undef>.
=item Any other defined scalar
The wrapped method returned a plain scalar in scalar context. Served
as-is in scalar context. If the caller subsequently asks for the
same key in list context, the scalar cannot be adapted -- the call is
treated as a miss and the method is re-invoked so the array form gets
independently cached.
=back
Handles C<DESTROY> specially: removes the wrapper from the double-wrap
registry and clears cache entries whose keys begin with C<$self->{_class}>
(Invariant I3 guarantees this is always set), then returns without
calling the inner object's DESTROY.
=cut | |||||
| 536 | ||||||
| 537 | sub AUTOLOAD | |||||
| 538 | { | |||||
| 539 | 2542 | 83536 | our $AUTOLOAD; | |||
| 540 | 2542 | 4317 | my ($method) = $AUTOLOAD =~ /::(\w+)\z/; | |||
| 541 | ||||||
| 542 | # Prevent side effects in the inner object's method from leaking $_ back | |||||
| 543 | # to the caller. Neither the DESTROY branch nor _can_fixate depends on a | |||||
| 544 | # pre-existing $_ value, so localizing it here is always safe. | |||||
| 545 | 2542 | 1649 | local $_; | |||
| 546 | ||||||
| 547 | 2542 | 1586 | my $self = shift; | |||
| 548 | 2542 | 1642 | my $cache = $self->{cache}; | |||
| 549 | 2542 | 1542 | my $wantlist = wantarray; # hoist: Perl does not cache this; avoid 3-4 redundant calls | |||
| 550 | ||||||
| 551 | # DESTROY arrives here because we handle it dynamically rather than | |||||
| 552 | # defining a named sub (which would suppress Class::Simple's AUTOLOAD). | |||||
| 553 | 2542 | 1989 | if($method eq 'DESTROY') { | |||
| 554 | # During global destruction the symbol table may already be torn | |||||
| 555 | # down; accessing the cache at that point is unsafe. | |||||
| 556 | 140 | 897 | return if defined($^V) && ($^V ge 'v5.14.0') && ${^GLOBAL_PHASE} eq 'DESTRUCT'; | |||
| 557 | ||||||
| 558 | # Remove from the double-wrap registry to prevent memory leaks in | |||||
| 559 | # long-running processes that create and destroy many wrappers. | |||||
| 560 | 140 | 293 | delete $cached{$self->{object}} if ref($self->{object}); | |||
| 561 | ||||||
| 562 | 140 | 166 | if($cache) { | |||
| 563 | 138 | 139 | if($self->{_cache_is_hash}) { | |||
| 564 | # Only delete keys that belong to this instance's class, | |||||
| 565 | # leaving entries from other classes untouched. | |||||
| 566 | 121 | 92 | my $prefix = $self->{_class}; | |||
| 567 | 121 1074 121 | 75 982 217 | delete $cache->{$_} for grep { index($_, $prefix) == 0 } keys %{$cache}; | |||
| 568 | } else { | |||||
| 569 | 17 | 39 | $cache->purge(); | |||
| 570 | } | |||||
| 571 | } | |||||
| 572 | 140 | 747 | return; | |||
| 573 | } | |||||
| 574 | ||||||
| 575 | # Build the cache key. | |||||
| 576 | # - _class is pre-computed in new() so we avoid ref($self) on every call. | |||||
| 577 | # - The @_ guard skips grep+join entirely for the common zero-arg getter case. | |||||
| 578 | 2402 | 1629 | my $key = $self->{_class} . '::' . $method . '::'; | |||
| 579 | 2402 2266 | 2001 2088 | $key .= join('::', grep { defined } @_) if @_; | |||
| 580 | ||||||
| 581 | # Use the pre-built backend closures (_get/_set) rather than a runtime | |||||
| 582 | # ref($cache) branch: the backend was fixed at construction time. | |||||
| 583 | 2402 | 1582 | my $get = $self->{_get}; | |||
| 584 | 2402 | 1518 | my $set = $self->{_set}; | |||
| 585 | ||||||
| 586 | 2402 | 1691 | my $cached_val = $get->($key); | |||
| 587 | 2401 | 2940 | if(defined($cached_val)) { | |||
| 588 | # Premise: $cached_val is defined, so it was previously stored. | |||||
| 589 | # The stored value is one of exactly three mutually exclusive forms: | |||||
| 590 | # (a) ARRAY ref -- method returned a list | |||||
| 591 | # (b) UNDEF_SENTINEL -- method returned undef or empty list | |||||
| 592 | # (c) any other scalar -- method returned a plain scalar | |||||
| 593 | # Each is a separate equivalence partition; no overlap is possible. | |||||
| 594 | ||||||
| 595 | 1083 | 834 | if(ref($cached_val) eq 'ARRAY') { | |||
| 596 | # Premise: a list result was cached. | |||||
| 597 | # Conclusion: serve it in either call context. | |||||
| 598 | 15 | 17 | $self->{_hits}{$key}++; | |||
| 599 | 15 10 | 23 21 | return $wantlist ? @{$cached_val} : $cached_val->[-1]; | |||
| 600 | } | |||||
| 601 | 1068 | 768 | if($cached_val eq $UNDEF_SENTINEL) { | |||
| 602 | # Premise: the cached result was undef or an empty list. | |||||
| 603 | # Conclusion: return "nothing" regardless of call context. | |||||
| 604 | 13 | 18 | $self->{_hits}{$key}++; | |||
| 605 | 13 | 22 | return; | |||
| 606 | } | |||||
| 607 | 1055 | 773 | if(!$wantlist) { | |||
| 608 | # Premise: a scalar is cached AND the caller wants a scalar. | |||||
| 609 | # Conclusion: exact hit -- return the scalar directly. | |||||
| 610 | 1050 | 856 | $self->{_hits}{$key}++; | |||
| 611 | 1050 | 990 | return $cached_val; | |||
| 612 | } | |||||
| 613 | # Premise: a scalar is cached BUT the caller now wants a list. | |||||
| 614 | # Conclusion: this cannot be served from cache (see LIMITATIONS). | |||||
| 615 | # Fall through to re-invoke in list context so the array form | |||||
| 616 | # gets cached. This is NOT counted as a hit. | |||||
| 617 | } | |||||
| 618 | ||||||
| 619 | # Premise: no usable cached value exists for this key+context combination. | |||||
| 620 | # Conclusion: invoke the real object and cache whatever it returns. | |||||
| 621 | 1323 | 1495 | $self->{_misses}{$key}++; | |||
| 622 | 1323 | 867 | my $object = $self->{object}; | |||
| 623 | ||||||
| 624 | 1323 | 951 | if($wantlist) { | |||
| 625 | 24 | 43 | my @result = $object->$method(@_); | |||
| 626 | 24 | 64 | if(!@result) { | |||
| 627 | # Premise: method returned an empty list (or undef in list context). | |||||
| 628 | # Conclusion: store the sentinel so future calls are hits, not misses. | |||||
| 629 | 3 | 5 | $set->($key, $UNDEF_SENTINEL); | |||
| 630 | 3 | 85 | return; | |||
| 631 | } | |||||
| 632 | # Premise: @result is non-empty. | |||||
| 633 | # Conclusion: cache as an arrayref so we can distinguish "list result" | |||||
| 634 | # (stored as ARRAY ref) from "scalar result" (stored as plain scalar). | |||||
| 635 | # fixate() shares memory across identical string values -- skip if any | |||||
| 636 | # element is a type that Data::Reuse cannot handle (RT#100461). | |||||
| 637 | 21 | 35 | Data::Reuse::fixate(@result) if _can_fixate(@result); | |||
| 638 | 21 | 971 | $set->($key, \@result); | |||
| 639 | 21 | 302 | return @result; | |||
| 640 | } | |||||
| 641 | ||||||
| 642 | 1299 | 1202 | my $result = $object->$method(@_); | |||
| 643 | 1295 | 2554 | if(!defined($result)) { | |||
| 644 | # Premise: method returned undef in scalar context. | |||||
| 645 | # Conclusion: store sentinel; a bare undef in the cache would be | |||||
| 646 | # indistinguishable from "cache miss" on the next call. | |||||
| 647 | 7 | 11 | $set->($key, $UNDEF_SENTINEL); | |||
| 648 | 7 | 7 | return; | |||
| 649 | } | |||||
| 650 | # Premise: $result is a defined scalar. | |||||
| 651 | # Conclusion: store it directly; ref($result) eq 'ARRAY' is impossible here | |||||
| 652 | # because the scalar branch is only reached when !$wantlist. | |||||
| 653 | 1288 | 1199 | $set->($key, $result); | |||
| 654 | 1287 | 1881 | return $result; | |||
| 655 | } | |||||
| 656 | ||||||
| 657 - 845 | =head1 LIMITATIONS
=over 4
=item B<Not safe for mutable objects>
The cache is never invalidated automatically. If the inner object's
state changes after caching, the wrapper will return stale data. The
caller must either reset the cache manually or avoid using this module
with objects that mutate.
=item B<Argument serialisation is naive>
Cache keys are built by joining defined arguments with C<::>. Two
different argument lists can therefore produce the same key if an
argument itself contains C<::> (e.g. C<foo('a::b', 'c')> vs
C<foo('a', 'b::c')>). Callers that pass arguments containing C<::>
should use a CHI backend with a custom key serialiser.
=item B<Undefined arguments are collapsed>
Undefined values in the argument list are silently dropped from the
cache key, so C<foo(undef)> and C<foo()> share a cache entry.
=item B<Scalar-then-list context mismatch is a miss>
If a method is first called in scalar context and then in list
context with identical arguments, the second call is a cache miss
and re-invokes the inner object. Both results are then independently
cached.
=item B<C<can('new')> returns a code reference, not a boolean>
For strict correctness C<can> returns C<\&new> for the C<'new'>
method rather than the boolean C<1>. The code reference is callable
but callers who compare it with C<==> to C<1> will see a mismatch.
=item B<Does not work with L<Memoize>>
C<Memoize> intercepts at the symbol-table level and conflicts with
the C<AUTOLOAD> dispatch used here.
=back
=head1 AUTHOR
Nigel Horne, C<< <njh at nigelhorne.com> >>
=head1 BUGS
Please report any bugs or feature requests to
L<https://github.com/nigelhorne/Class-Simple-Readonly-Cached/issues>.
=head1 SEE ALSO
=over 4
=item * L<Test Dashboard|https://nigelhorne.github.io/Class-Simple-Readonly-Cached/coverage/>
=item * L<Class::Simple>
=item * L<CHI>
=item * L<Data::Reuse>
Values are shared between C<Class::Simple::Readonly::Cached> objects,
since they are read-only.
=item * L<constant::defer>
=back
=head1 SUPPORT
This module is provided as-is without any warranty.
You can find documentation for this module with the perldoc command.
perldoc Class::Simple::Readonly::Cached
=over 4
=item * MetaCPAN
L<https://metacpan.org/release/Class-Simple-Readonly-Cached>
=item * Source Repository
L<https://github.com/nigelhorne/Class-Simple-Readonly-Cached>
=item * CPAN Testers
L<http://matrix.cpantesters.org/?dist=Class-Simple-Readonly-Cached>
=back
=encoding UTF-8
=head1 FORMAL SPECIFICATION
=head2 new
new : (C x P) -> (W | undef)
C = class name string
P = { cache : (HashRef | CacheObj), object? : Ref, quiet? : Bool, ... }
W = blessed P in C
valid_cache(c) :=
ref(c) = 'HASH'
OR ( blessed(c) AND c.can('get') AND c.can('set') AND c.can('purge') )
Precondition:
valid_cache(P.cache)
Post-construction invariants (hold for all W returned by new()):
W._class = C
W._cache_is_hash = (ref(P.cache) = 'HASH')
W._get = λk. (W._cache_is_hash ? W.cache[k] : W.cache.get(k))
W._set = λ(k,v). (W._cache_is_hash ? W.cache[k]:=v
: W.cache.set(k,v,'never'))
Double-wrap invariant:
forall o in Dom(cached): new(C, {object: o, ...}) = cached[o].object
Clone (object invocation):
forall w : W: w.new(P') = bless( merge(w, P'), ref(w) )
Corollary: if cache in Dom(P'), rebuild _get/_set/_cache_is_hash for P'.cache
=head2 object
object : W -> Ref
forall w : W: object(w) = w.object
=head2 state
state : W -> HashRef
forall w : W: state(w) = { hits => w._hits, misses => w._misses }
=head2 can
can : (W|Str x Str) -> (CodeRef | undef)
forall w : W, m : Str:
can(w, 'new') = \&new
can(w, m) = w.object.can(m) OR SUPER::can(w, m)
=head2 isa
isa : (W x Str) -> Bool
forall w : W, c : Str:
isa(w, c) = 1 if c in { ref(w), 'Class::Simple::Readonly::Cached' }
| 1 if SUPER::isa(w, c)
| w.object.isa(c) if ref(w)
| 0 otherwise
=head2 autoload
autoload : (W x M x A*) -> R
M = method name string
A* = argument tuple (possibly empty)
R = scalar | list | undef
Cache key:
k(w, m, a) := w._class ++ '::' ++ m ++ '::' ++ defined_args(a)
(w._class = ref(w), pre-computed once in new() to avoid ref() per dispatch)
Caching law:
get(cache(w), k(w,m,a)) = v, v != undef
=> autoload(w, m, a) = v (cache hit)
get(cache(w), k(w,m,a)) = undef
=> v = w.object.m(a)
set(cache(w), k(w,m,a), v)
autoload(w, m, a) = v (cache miss)
=head1 LICENSE AND COPYRIGHT
Author Nigel Horne: C<njh@nigelhorne.com>
Copyright (C) 2019-2026 Nigel Horne
Usage is subject to the GPL2 licence terms.
If you use it,
please let me know.
=cut | |||||
| 846 | ||||||
| 847 | 1; | |||||