# NAME

Dancer2::Plugin::ContentCache - Cache HTML/JSON responses for later use.

# VERSION

version 1.0000

# SYNOPSIS

    # In config.yml:
    #
    plugins:
      ContentCache:
         # REQUIRED, unless you supply your own 'driver' (see DRIVERS, below).
         cache_result_set: ContentCache

         # Optional, with defaults
         driver: DBIx::Class
         schema: default
         create_redirect_route: 1
         redirect_route: '/recall'
         require_login: 0
         cache_aging: 1
         default_life: 86400

    # In your Dancer2 application
    #
    package MyApp;
    use Dancer2 appname => 'MyApp';
    use Dancer2::Plugin::ContentCache;

    post '/this_route' => sub {
      # This route is supposed to return HTML

      my $html = template 'Foo',
                 { param1 => 'bar' },
                 { layout => 'some_layout'};

      cache_and_redirect $html,
                 { life => 6000, created_by => 'baz' };
                 # metadata; note 'life'
    };

    post '/this_other_route' => sub {
      # do some processing
      # This route is supposed to return JSON

      cache_and_send $results, { created_by => 'baz' };
      # $results is a hashref or arrayref.
    };

    #
    # Example of custom cache handler:
    #
    get '/retrieve_privileged/:uuid' => sub {

       my $cache = retrieve_cache route_parameters->get('uuid');
       my $all_good = 0;

       # Use the stuff in $cache->metadata to decide if you want to send this
       # to the user, and set $all_good as appropriate.

       if ($all_good) {
          send_as $cache->data_format => $cache->data;
       }
       else {
          return send_error('Not Found', 404);
       }
    };

# DESCRIPTION

This plugin for [Dancer2](https://metacpan.org/pod/Dancer2) implements a response-content cache and optionally
issues a redirect to that content. In particular, this is useful after a POST
route; if the network times out (as can happen on wonky connections), you
don't want the user to re-POST. By redirecting them to the now-cached content
as a GET, you prevent re-POSTing.

Storage is never touched directly by this plugin; every read and write goes
through a small driver object, so you are not locked into [DBIx::Class](https://metacpan.org/pod/DBIx%3A%3AClass). See
["DRIVERS"](#drivers), below.

# CONFIGURATION

In your `config.yml` for your Dancer2 application, use these configuration
parameters:

- `driver`

    This optional configuration parameter is to allow use of some other driver
    for the database than the default, which is [DBIx::Class](https://metacpan.org/pod/DBIx%3A%3AClass). See ["DRIVERS"](#drivers).

- `schema`

    This parameter lets you specify which schema is the one containing your
    result set (table). Default value is 'default', which is what's usually used
    when you only have one. Only used by the default [DBIx::Class](https://metacpan.org/pod/DBIx%3A%3AClass) driver, which
    will use whichever of [Dancer2::Plugin::DBIC](https://metacpan.org/pod/Dancer2%3A%3APlugin%3A%3ADBIC) or
    [Dancer2::Plugin::DBIx::Class](https://metacpan.org/pod/Dancer2%3A%3APlugin%3A%3ADBIx%3A%3AClass) your application has loaded (preferring
    `DBIC` if both are present).

- `cache_result_set`

    **Mandatory**, if the `driver` is the default [DBIx::Class](https://metacpan.org/pod/DBIx%3A%3AClass). This setting
    points at the ResultSet name in which the table will be stored.

- `create_redirect_route`

    Default is `1`, true. The system will create a default route to retrieve and
    return the cache. The name will be set in `redirect_route`, below, and will
    expect a single route parameter, the UUID of the cache entry.

    If set to `0`, false, then no route will be created, and it's assumed you
    will roll your own, using the `retrieve_cache` keyword below.

- `redirect_route`

    The name of the built-in redirect route, if it is created. Default is
    '/recall'.

- `require_login`

    If the built-in redirect route is created, is login required? If this is set
    to `1`, true, AND you are using [Dancer2::Plugin::Auth::Extensible](https://metacpan.org/pod/Dancer2%3A%3APlugin%3A%3AAuth%3A%3AExtensible), then
    the created route will check for `logged_in_user` before returning the data.
    If you are not using the Extensible plugin, or the user is not logged in,
    they will be returned a 404 error.

- `cache_aging`

    Default is `1`, true. Set this to `0`, false, to disable cache aging. Note
    in ["BUGS AND LIMITATIONS"](#bugs-and-limitations), below, that if you have aging on, your driver
    must be able to record the two necessary fields. The plugin will `croak` at
    application start time, if the necessary bits aren't there.

- `default_life`

    Default is 86400 seconds (1 day). If `cache_aging` is on, this sets the
    default life of the cache entries, if not specified by `life` metadata in
    the cache entry.

# DANCER2 KEYWORDS

- **cache\_and\_send**

    This keyword replaces `send_as [html|JSON]` in your Dancer2 route to first
    cache the required data, then ship it along without a redirect pointing at
    the cache.

- **cache\_and\_redirect**

    This keyword also replaces `send_as [html|JSON]` in your Dancer2 route to
    first cache the required data, then redirect the user to the cache-retrieval
    link.

- **retrieve\_cache**

    Given a UUID of the cache, attempts to retrieve the cache with that key. If
    the cache is already expired, or doesn't exist under that UUID, return
    undef. With an unexpired cache, this keyword returns a cache object (see
    ["THE CACHE OBJECT"](#the-cache-object) for details).

- **set\_cache**

    This is a convenience method for pushing a cache. It expects either some
    HTML (a scalar) or JSON (a hashref or arrayref), and an additional hashref
    of optional metadata, and returns the UUID of the cache created.

        # A JSON cache, with five minutes of life.
        my $uuid = set_cache $hashref, { life => 300 };

        # Another JSON cache, this time of an arrayref, with default life.
        my $uuid = set_cache $arrayref, {};

        # An html/scalar cache, with the default life.
        my $uuid = set_cache $html;

- **clean\_up\_cache**

    This convenience term should be run from time to time if `cache_aging` is
    turned on. It will look through the store and delete all expired caches,
    returning the number of expired entities. If `cache_aging` is off, it will
    always return zero. You might put this in an admin route for super-users, or
    it can be included in a cron job to run from time to time.

# THE CACHE OBJECT

The cache object, [Dancer2::Plugin::ContentCache::CacheEntry](https://metacpan.org/pod/Dancer2%3A%3APlugin%3A%3AContentCache%3A%3ACacheEntry), is a simple
[Moo](https://metacpan.org/pod/Moo) object containing the following field/methods:

- **data\_format**

    This will return `html` if the original data sent into the cache was HTML
    or some other scalar, and `JSON` if a hashref or arrayref was put in the
    cache. Useful for `send_as`-ing a retrieved cache.

- **data**

    The data that was placed in the cache, exactly as it was sent in with
    `cache_and_send` or `set_cache`--either a scalar, arrayref, or hashref.

- **created\_dt**

    If the driver is able to record a creation time (see ["DRIVERS"](#drivers)), it is
    automatically set at cache create time, and may be retrieved here as a
    [DateTime](https://metacpan.org/pod/DateTime) object. If not, this will return undefined. This behavior
    (unlike `expiry_dt` below) is **not** dependent on the value of
    `cache_aging`, merely the driver's capability.

- **expiry\_dt**

    If `cache_aging` is turned on, this will recall the [DateTime](https://metacpan.org/pod/DateTime) that the
    cache will expire. If not, this will return undefined.

- **metadata**

    A hashref containing all the metadata stored with this object. The `life`
    will always be present if the `cache_aging` setting is turned on.

    Note that `life` is the only reserved metadata term. If aging is turned on,
    it will be automatically set by the default unless you override it with a
    value.

# DRIVERS

[Dancer2::Plugin::ContentCache](https://metacpan.org/pod/Dancer2%3A%3APlugin%3A%3AContentCache) never touches a database (or anything else)
directly; all storage happens through a driver class that consumes the
[Dancer2::Plugin::ContentCache::Driver](https://metacpan.org/pod/Dancer2%3A%3APlugin%3A%3AContentCache%3A%3ADriver) role. The bundled default,
[Dancer2::Plugin::ContentCache::Driver::DBIC](https://metacpan.org/pod/Dancer2%3A%3APlugin%3A%3AContentCache%3A%3ADriver%3A%3ADBIC), stores entries with
[DBIx::Class](https://metacpan.org/pod/DBIx%3A%3AClass) via [Dancer2::Plugin::DBIx::Class](https://metacpan.org/pod/Dancer2%3A%3APlugin%3A%3ADBIx%3A%3AClass) or [Dancer2::Plugin::DBIC](https://metacpan.org/pod/Dancer2%3A%3APlugin%3A%3ADBIC),
whichever your application is using..

To use a different backend, write a class that consumes
[Dancer2::Plugin::ContentCache::Driver](https://metacpan.org/pod/Dancer2%3A%3APlugin%3A%3AContentCache%3A%3ADriver) and set the `driver` configuration
option to its full package name:

    plugins:
      ContentCache:
        driver: MyApp::ContentCache::Driver::Redis

See [Dancer2::Plugin::ContentCache::Driver](https://metacpan.org/pod/Dancer2%3A%3APlugin%3A%3AContentCache%3A%3ADriver) for the small set of methods
your driver needs to implement.

# SUGGESTED SCHEMA

Here's the sort of schema you'll need for the default [DBIx::Class](https://metacpan.org/pod/DBIx%3A%3AClass) driver
(this one, for PostgreSQL). If you're using more-recent versions of
PostgreSQL that have a UUID type, you could use that, but this is the
lowest-common-denominator form of the schema:

    CREATE TABLE IF NOT EXISTS content_cache (
      uuid TEXT NOT NULL PRIMARY KEY,         -- Or "id", your choice.
      metadata TEXT NOT NULL,                 -- REQUIRED
      data TEXT NOT NULL,                     -- REQUIRED
      created_dt TIMESTAMP WITHOUT TIME ZONE, -- Plugin will always assume "local"
                                              -- unless you store it WITH TIMEZONE
                                              -- Only needed if cache_aging is on.
      expiry_dt TIMESTAMP WITHOUT TIME ZONE   -- Same here as with created_dt
    );

# DEPENDENCIES

- [Dancer2](https://metacpan.org/pod/Dancer2), obviously.
- [Dancer2::Plugin::DBIC](https://metacpan.org/pod/Dancer2%3A%3APlugin%3A%3ADBIC) or [Dancer2::Plugin::DBIx::Class](https://metacpan.org/pod/Dancer2%3A%3APlugin%3A%3ADBIx%3A%3AClass), if you're using
the default driver.
- [DateTime](https://metacpan.org/pod/DateTime)
- [DateTime::Format::Strptime](https://metacpan.org/pod/DateTime%3A%3AFormat%3A%3AStrptime), if you're using the default driver.
- [JSON::MaybeXS](https://metacpan.org/pod/JSON%3A%3AMaybeXS), which requires any one of three different JSON processors;
you probably already have at least one of them installed.
- [Module::Runtime](https://metacpan.org/pod/Module%3A%3ARuntime)
- [UUID::Tiny](https://metacpan.org/pod/UUID%3A%3ATiny)

# BUGS AND LIMITATIONS

- This tool provides no mechanism for "refreshing" or "updating" a cache. Once
an entry is created, it is immutable, both in content and in its expiry.
That may be a limitation, but we may also call it a feature, depending on
your mindset.
- If you specify `cache_aging`, but your driver cannot record `created_dt`
and `expiry_dt` (for the default DBIx::Class driver: your schema does not
have those two columns), aging won't work!

# ACKNOWLEDGEMENTS

The idea for this plugin comes from Mike Weisenborn of
[Clearbuilt](https://clearbuilt.com). Clearbuilt graciously sponsored the
development of this work.

# SEE ALSO

- [Dancer2::Plugin::CHI](https://metacpan.org/pod/Dancer2%3A%3APlugin%3A%3ACHI) is a less-feature-rich key-value cache that could be
used for some of what we're doing here. If this one is doing too much for
your tastes, it's a good choice.
- [Dancer2::Plugin::ViewCache](https://metacpan.org/pod/Dancer2%3A%3APlugin%3A%3AViewCache) also stores HTML page content, but is
deliberately intended for "guest" viewing without requiring a login.

# AUTHOR

D Ruth Holloway <ruth@hiruthie.me>

# COPYRIGHT AND LICENSE

This software is copyright (c) 2026 by D Ruth Holloway.

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.
