NAME

Class::Simple::Readonly::Cached - cache messages to an object

VERSION

Version 0.13

SYNOPSIS

A caching decorator for 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.

DESCRIPTION

Wraps any Perl object in a transparent caching layer. Every method call is intercepted via AUTOLOAD; on the first call (a miss) the result is stored in the cache and returned. Subsequent identical calls (same method name, same argument list) are 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).

SUBROUTINES/METHODS

new

Construct a caching proxy around any Perl object.

Arguments

Returns

A Class::Simple::Readonly::Cached object, or undef on invalid object. Croaks on invalid cache.

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

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 }

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.

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

object

Return the inner (wrapped) object.

Returns

The blessed reference that was passed as object to new().

EXAMPLE

# Bypass the cache to mutate state directly.
$cached->object()->reset();

API SPECIFICATION

# Input  none
# Output { type => 'object' }

MESSAGES

(none)

state

Return a snapshot of cache hit and miss counts per cache key. Primarily useful for performance profiling and white-box tests.

Returns

A hash reference:

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;

API SPECIFICATION

# Input  None
# Output { type => 'hashref',
#          keys => { hits   => 'hashref|undef',
#                    misses => 'hashref|undef' } }

MESSAGES

(none)

can

Report whether the inner object (or this class) can respond to a given method. Overrides UNIVERSAL::can to account for the decorator pattern.

Returns

A code reference if the method exists, undef otherwise.

EXAMPLE

my $code = $cached->can('compute');
$code->($cached) if $code;

API SPECIFICATION

# Input  { self   => { type => 'object|string' },
#          method => { type => 'string' } }
# Output { type => 'coderef|undef' }

MESSAGES

(none)

isa

Test class membership, delegating to the inner object's class hierarchy when needed. Overrides UNIVERSAL::isa to support the transparent decorator pattern.

Returns

True if the wrapper or its inner object is-a $class.

EXAMPLE

$cached->isa('My::Domain::Object');   # true if inner object is

API SPECIFICATION

# Input  { self  => { type => 'object|string' },
#          class => { type => 'string' } }
# Output { type => 'bool' }

MESSAGES

(none)

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 _get/_set coderefs installed by _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:

Handles DESTROY specially: removes the wrapper from the double-wrap registry and clears cache entries whose keys begin with $self-{_class}> (Invariant I3 guarantees this is always set), then returns without calling the inner object's DESTROY.

LIMITATIONS

AUTHOR

Nigel Horne, <njh at nigelhorne.com>

BUGS

Please report any bugs or feature requests to https://github.com/nigelhorne/Class-Simple-Readonly-Cached/issues.

SEE ALSO

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

FORMAL SPECIFICATION

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

object

object : W -> Ref

forall w : W: object(w) = w.object

state

state : W -> HashRef

forall w : W: state(w) = { hits => w._hits, misses => w._misses }

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)

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

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)

LICENSE AND COPYRIGHT

Author Nigel Horne: 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.