Skip to content

Commit

Permalink
feat: added type-safety to time-based revalidation
Browse files Browse the repository at this point in the history
commit 3ccc6e7
Merge: 0983d68 12d735e
Author: arctic_hen7 <[email protected]>
Date:   Mon Jul 11 14:29:06 2022 +1000

    chore: resolved merge conflicts

commit 12d735e
Author: Vukašin Stepanović <[email protected]>
Date:   Thu Jan 13 10:55:40 2022 +0100

    Add Duration struct that parses time strings

commit 06e8d55
Author: Vukašin Stepanović <[email protected]>
Date:   Wed Jan 12 11:40:43 2022 +0100

    Use newtype instead of string for time revalidation
  • Loading branch information
arctic-hen7 committed Jul 11, 2022
1 parent 0983d68 commit 7b3ff88
Show file tree
Hide file tree
Showing 7 changed files with 115 additions and 78 deletions.
3 changes: 2 additions & 1 deletion examples/core/state_generation/src/templates/revalidation.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use perseus::{RenderFnResultWithCause, Template};
use sycamore::prelude::{view, Html, Scope, View};
use std::time::Duration;

#[perseus::make_rx(PageStateRx)]
pub struct PageState {
Expand All @@ -17,7 +18,7 @@ pub fn get_template<G: Html>() -> Template<G> {
Template::new("revalidation")
.template(revalidation_page)
// This page will revalidate every five seconds (and so the time displayed will be updated)
.revalidate_after("5s".to_string())
.revalidate_after(Duration::new(5, 0))
// This is an alternative method of revalidation that uses logic, which will be executed
// every itme 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

use perseus::{RenderFnResult, RenderFnResultWithCause, Template};
use sycamore::prelude::{view, Html, Scope, View};
use std::time::Duration;

#[perseus::make_rx(PageStateRx)]
pub struct PageState {
Expand All @@ -23,7 +24,7 @@ pub fn get_template<G: Html>() -> Template<G> {
Template::new("revalidation_and_incremental_generation")
.template(revalidation_and_incremental_generation_page)
// This page will revalidate every five seconds (and so the time displayed will be updated)
.revalidate_after("5s".to_string())
.revalidate_after(Duration::new(5, 0))
// This is an alternative method of revalidation that uses logic, which will be executed
// every itme 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
Expand Down
6 changes: 4 additions & 2 deletions packages/perseus/src/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ use crate::stores::{ImmutableStore, MutableStore};
use crate::template::Template;
use crate::template::{PageProps, TemplateMap};
use crate::translator::Translator;
use crate::utils::decode_time_str;
use futures::future::try_join_all;
use std::collections::HashMap;
use sycamore::prelude::SsrNode;
Expand Down Expand Up @@ -205,7 +204,10 @@ async fn gen_state_for_path(
// We don't need to worry about revalidation that operates by logic, that's
// request-time only
if template.revalidates_with_time() {
let datetime_to_revalidate = decode_time_str(&template.get_revalidate_interval().unwrap())?;
let datetime_to_revalidate = template
.get_revalidate_interval()
.unwrap()
.compute_timestamp();
// Write that to a static file, we'll update it every time we revalidate
// Note that this runs for every path generated, so it's fully usable with ISR
// Yes, there's a different revalidation schedule for each locale, but that
Expand Down
17 changes: 10 additions & 7 deletions packages/perseus/src/server/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ use crate::page_data::PageData;
use crate::stores::{ImmutableStore, MutableStore};
use crate::template::{PageProps, States, Template, TemplateMap};
use crate::translator::Translator;
use crate::utils::decode_time_str;
use crate::Request;
use crate::SsrNode;
use chrono::{DateTime, Utc};
Expand Down Expand Up @@ -202,10 +201,12 @@ async fn revalidate(
// We don't need to worry about revalidation that operates by logic, that's
// request-time only
if template.revalidates_with_time() {
// IMPORTANT: we set the new revalidation datetime to the interval from NOW, not
// from the previous one So if you're revalidating many pages weekly,
// they will NOT revalidate simultaneously, even if they're all queried thus
let datetime_to_revalidate = decode_time_str(&template.get_revalidate_interval().unwrap())?;
// IMPORTANT: we set the new revalidation datetime to the interval from NOW, not from the previous one
// So if you're revalidating many pages weekly, they will NOT revalidate simultaneously, even if they're all queried thus
let datetime_to_revalidate = template
.get_revalidate_interval()
.unwrap()
.compute_timestamp();
mutable_store
.write(
&format!("static/{}.revld.txt", path_encoded),
Expand Down Expand Up @@ -366,8 +367,10 @@ pub async fn get_page_for_template<M: MutableStore, T: TranslationsManager>(
// request-time only Obviously we don't need to revalidate
// now, we just created it
if template.revalidates_with_time() {
let datetime_to_revalidate =
decode_time_str(&template.get_revalidate_interval().unwrap())?;
let datetime_to_revalidate = template
.get_revalidate_interval()
.unwrap()
.compute_timestamp();
// Write that to a static file, we'll update it every time we revalidate
// Note that this runs for every path generated, so it's fully usable with
// ISR
Expand Down
19 changes: 10 additions & 9 deletions packages/perseus/src/template/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ use http::header::HeaderMap;
use sycamore::prelude::{Scope, View};
#[cfg(not(target_arch = "wasm32"))]
use sycamore::utils::hydrate::with_no_hydration_context;
use crate::utils::ComputedDuration;
#[cfg(not(target_arch = "wasm32"))]
use std::time::Duration;

/// A generic error type that can be adapted for any errors the user may want to
/// return from a render function. `.into()` can be used to convert most error
Expand Down Expand Up @@ -175,16 +178,14 @@ pub struct Template<G: Html> {
/// request that invoked it.
#[cfg(not(target_arch = "wasm32"))]
should_revalidate: Option<ShouldRevalidateFn>,
/// A length of time after which to prerender the template again. This
/// should specify a string interval to revalidate after. That will be
/// converted into a datetime to wait for, which will be updated after
/// every revalidation. Note that, if this is used with incremental
/// A length of time after which to prerender the template again. The given duration will be waited for,
/// and the next request after it will lead to a revalidation. Note that, if this is used with incremental
/// generation, the counter will only start after the first render
/// (meaning if you expect a weekly re-rendering cycle for all pages,
/// they'd likely all be out of sync, you'd need to manually implement
/// that with `should_revalidate`).
#[cfg(not(target_arch = "wasm32"))]
revalidate_after: Option<String>,
revalidate_after: Option<ComputedDuration>,
/// Custom logic to amalgamate potentially different states generated at
/// build and request time. This is only necessary if your template uses
/// both `build_state` and `request_state`. If not specified and both are
Expand Down Expand Up @@ -438,7 +439,7 @@ impl<G: Html> Template<G> {
}
/// Gets the interval after which the template will next revalidate.
#[cfg(not(target_arch = "wasm32"))]
pub fn get_revalidate_interval(&self) -> Option<String> {
pub fn get_revalidate_interval(&self) -> Option<ComputedDuration> {
self.revalidate_after.clone()
}

Expand Down Expand Up @@ -640,8 +641,8 @@ impl<G: Html> Template<G> {
/// - y: year (365 days always, leap years ignored, if you want them add
/// them as days)
#[cfg(not(target_arch = "wasm32"))]
pub fn revalidate_after(mut self, val: String) -> Template<G> {
self.revalidate_after = Some(val);
pub fn revalidate_after<I: Into<Duration>>(mut self, val: I) -> Template<G> {
self.revalidate_after = Some(ComputedDuration::new(val));
self
}
/// Enables the *revalidation* strategy (time variant). This takes a time
Expand All @@ -656,7 +657,7 @@ impl<G: Html> Template<G> {
/// - y: year (365 days always, leap years ignored, if you want them add
/// them as days)
#[cfg(target_arch = "wasm32")]
pub fn revalidate_after(self, _val: String) -> Template<G> {
pub fn revalidate_after<I: Into<Duration>>(self, _val: I) -> Template<G> {
self
}

Expand Down
143 changes: 86 additions & 57 deletions packages/perseus/src/utils/decode_time_str.rs
Original file line number Diff line number Diff line change
@@ -1,62 +1,91 @@
use crate::errors::*;
use chrono::{Duration, Utc};

// Decodes time strings like '1w' into actual datetimes from the present moment. If you've ever used NodeJS's [`jsonwebtoken`](https://www.npmjs.com/package/jsonwebtoken) module, this is
/// very similar (based on Vercel's [`ms`](https://github.com/vercel/ms) module for JavaScript).
/// Accepts strings of the form 'xXyYzZ...', where the lower-case letters are
/// numbers meaning a number of the intervals X/Y/Z (e.g. 1m4d -- one month four
/// days). The available intervals are:
use chrono::Utc;
use std::{convert::TryFrom, time};

/// Represents a duration that can be computed relative to the current time.
#[derive(Debug, Clone)]
pub struct ComputedDuration(chrono::Duration);

impl ComputedDuration {
/// Creates a new [`ComputedDuration`] from the given duration-like type.
pub fn new<I: Into<time::Duration>>(duration: I) -> Self {
let duration = chrono::Duration::from_std(duration.into()).unwrap();
Self(duration)
}

/// Get the timestamp of the duration added to the current time.
pub fn compute_timestamp(&self) -> String {
let current = Utc::now();
let datetime = current + self.0;
datetime.to_rfc3339()
}
}

/// A simpler representation of a duration based on individual components.
///
/// - s: second,
/// - m: minute,
/// - h: hour,
/// - d: day,
/// - w: week,
/// - M: month (30 days used here, 12M ≠ 1y!),
/// - y: year (365 days always, leap years ignored, if you want them add them as
/// days)
pub fn decode_time_str(time_str: &str) -> Result<String, BuildError> {
let mut duration_after_current = Duration::zero();
// Get the current datetime since Unix epoch, we'll add to that
let current = Utc::now();
// A working variable to store the '123' part of an interval until we reach the
// idnicator and can do the full conversion
let mut curr_duration_length = String::new();
// Iterate through the time string's characters to get each interval
for c in time_str.chars() {
// If we have a number, append it to the working cache
// If we have an indicator character, we'll match it to a duration
if c.is_numeric() {
curr_duration_length.push(c);
} else {
// Parse the working variable into an actual number
let interval_length = curr_duration_length.parse::<i64>().unwrap(); // It's just a string of numbers, we know more than the compiler
let duration = match c {
's' => Duration::seconds(interval_length),
'm' => Duration::minutes(interval_length),
'h' => Duration::hours(interval_length),
'd' => Duration::days(interval_length),
'w' => Duration::weeks(interval_length),
'M' => Duration::days(interval_length * 30), /* Multiplying the number of months */
// by 30 days (assumed length of a
// month)
'y' => Duration::days(interval_length * 365), /* Multiplying the number of years */
// by 365 days (assumed length of
// a year)
c => {
return Err(BuildError::InvalidDatetimeIntervalIndicator {
indicator: c.to_string(),
})
/// Note that months are assumed to be 30 days long, and years 365 days long.
#[derive(Default, Debug)]
pub struct Duration {
years: i64,
months: i64,
weeks: i64,
days: i64,
hours: i64,
minutes: i64,
seconds: i64,
}

/// An error type for invalid `String` durations.
pub struct InvalidDuration;

impl From<Duration> for time::Duration {
fn from(duration: Duration) -> Self {
let duration = chrono::Duration::seconds(duration.seconds)
+ chrono::Duration::minutes(duration.minutes)
+ chrono::Duration::hours(duration.hours)
+ chrono::Duration::days(duration.days)
+ chrono::Duration::weeks(duration.weeks)
+ chrono::Duration::days(duration.months * 30) // Assumed length of a month
+ chrono::Duration::days(duration.years * 365); // Assumed length of a year

duration.to_std().unwrap()
}
}

impl TryFrom<&str> for Duration {
type Error = InvalidDuration;

fn try_from(value: &str) -> Result<Self, Self::Error> {
let mut duration = Self::default();

// A working variable to store the '123' part of an interval until we reach the indicator and can do the full conversion
let mut curr_duration_length = String::new();

for c in value.chars() {
// If we have a number, append it to the working cache
// If we have an indicator character, we'll match it to a duration
if c.is_numeric() {
curr_duration_length.push(c);
} else {
let interval_length: i64 = curr_duration_length.parse().unwrap(); // It's just a string of numbers, we know more than the compiler
if interval_length <= 0 {
return Err(InvalidDuration);
}
};
duration_after_current = duration_after_current + duration;
// Reset that working variable
curr_duration_length = String::new();

match c {
's' if duration.seconds == 0 => duration.seconds = interval_length,
'm' if duration.minutes == 0 => duration.minutes = interval_length,
'h' if duration.hours == 0 => duration.hours = interval_length,
'd' if duration.days == 0 => duration.days = interval_length,
'w' if duration.weeks == 0 => duration.weeks = interval_length,
'M' if duration.months == 0 => duration.months = interval_length,
'y' if duration.years == 0 => duration.years = interval_length,
_ => return Err(InvalidDuration),
};

curr_duration_length = String::new();
}
}
}
// Form the final duration by reducing the durations vector into one
let datetime = current + duration_after_current;

// We return an easily parsible format (RFC 3339)
Ok(datetime.to_rfc3339())
Ok(duration)
}
}
2 changes: 1 addition & 1 deletion packages/perseus/src/utils/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@ pub(crate) use async_fn_trait::AsyncFnReturn;
pub use cache_res::{cache_fallible_res, cache_res};
pub(crate) use context::provide_context_signal_replace;
#[cfg(not(target_arch = "wasm32"))]
pub use decode_time_str::decode_time_str;
pub use decode_time_str::{ComputedDuration, Duration, InvalidDuration};
pub use path_prefix::*;

0 comments on commit 7b3ff88

Please sign in to comment.