Revalidation

Sometimes, you'll want to use build-time state generation, but you'll want to update the state you've generated at a later date. For example, let's say you have a website that lists the latest news, and build state is used to do that. If you want to update this news every hour, you could do that with revalidation! (This avoids much of the overhead of request-time state, which must be generated before every single page load, and has no opportunity for caching.)

Generally, if you can use it, revalidation will yield better performance than request-time state.

Time-based revalidation

The first type of revalidation is the simplest: you set a schedule with .revalidate_after() on Template, which takes either a Duration (from chrono or the standard library) or a string of the form <num><unit>, like 1h for one hour. You can read more about that here.

This will cause the Perseus build process to, for each page that this template generates, note down the current time, and write that to a file. Then, on each request, it will check if the current time is later than that recorded time, plus the revalidation interval. If so, then it will re-execute the build state function, and update the state accordingly. Templates using revalidation have their pages stored in the mutable store, since they may update later.

Crucially, this is lazy revalidation: Perseus will not immediately revalidate a page once the revalidation interval is reached. For example, if our news site isn't very popular for its first month, and only gets two visits per day, it won't revalidate 24 times, it will probably revalidate twice: because only two people visited. This also means that revalidation can behave in unexpected ways. Let's say you have a page that revalidates every five seconds, and it's built at second 0. If, no-one requests it until second 6, and then there's a request every second, it will revalidate at second 6, then second 11, then second 16, etc. You may need to re-read that to understand this, and it's usually not a problem, unles syou have very strict requirements.

Note that this is all page-specific, so it's entirely possible for two different pages in the same template to have teh same revalidation interval and revalidate at different times.

Logic-based revalidation

When you have more stringent needs, you might wish to use logic-based revalidation, which is based on the .should_revalidate_fn() method on Template. To this, you provide an async function of the usual sort with the usual BlamedError<E> error handling (see here for an explanation of that) that takes a StateGeneratorInfo instance and the user's request, and you return a bool: if it's true, the page will revalidate, but, if false, the old state will stand. This can be used to do more advanced things like having a database of new news, but also having a micro-site set to tell you whether or not there is new news. Thus, you can perform the quicker check to the micro-site (which acts as a canary) to avoid unnecessary revalidations, which will improve performance.

Using both logic-based revalidation and time-based revalidation is perfectly permissible, as the logic-based revalidation will only be executed on the interval of the time-based. For our news site, therefore, we might want to use the logic-based revalidation to check a canary as to whether or not there is any new news, and then only run that check hourly. This would lead to hourly checks of whether or not we should revalidate, rather than just blindly doing so, which can improve performance greatly.

Example

An example of using both logic-based and time-based revalidation together is below.

use perseus::prelude::*;
use serde::{Deserialize, Serialize};
use sycamore::prelude::*;

#[derive(Serialize, Deserialize, Clone, ReactiveState)]
#[rx(alias = "PageStateRx")]
struct PageState {
    time: String,
}

fn revalidation_page<'a, G: Html>(cx: BoundedScope<'_, 'a>, state: &'a PageStateRx) -> View<G> {
    view! { cx,
        p { (format!("The time when this page was last rendered was '{}'.", state.time.get())) }
    }
}

pub fn get_template<G: Html>() -> Template<G> {
    Template::build("revalidation")
        .view_with_state(revalidation_page)
        // This page will revalidate every five seconds (and so the time displayed will be updated)
        .revalidate_after("5s")
        // This is an alternative method of revalidation that uses logic, which will be executed
        // every time a user tries to load this page. For that reason, this should NOT do
        // long-running work, as requests will be delayed. If both this
        // and `revalidate_after()` are provided, this logic will only run when `revalidate_after()`
        // tells Perseus that it should revalidate.
        .should_revalidate_fn(should_revalidate)
        .build_state_fn(get_build_state)
        .build()
}

// This will get the system time when the app was built
#[engine_only_fn]
async fn get_build_state(_info: StateGeneratorInfo<()>) -> PageState {
    PageState {
        time: format!("{:?}", std::time::SystemTime::now()),
    }
}

// This will run every time `.revalidate_after()` permits the page to be
// revalidated This acts as a secondary check, and can perform arbitrary logic
// to check if we should actually revalidate a page.
//
// Since this takes the request, this uses a `BlamedError` if it's fallible.
#[engine_only_fn]
async fn should_revalidate(
    // This takes the same arguments as request state
    _info: StateGeneratorInfo<()>,
    _req: perseus::Request,
) -> Result<bool, BlamedError<std::convert::Infallible>> {
    // For simplicity's sake, this will always say we should revalidate, but you
    // could make this check any condition
    Ok(true)
}