Skip to content

Commit

Permalink
Use newtype instead of string for time revalidation
Browse files Browse the repository at this point in the history
  • Loading branch information
Asha20 committed Jan 13, 2022
1 parent 69e9f72 commit 06e8d55
Show file tree
Hide file tree
Showing 6 changed files with 43 additions and 67 deletions.
4 changes: 3 additions & 1 deletion examples/showcase/src/templates/time.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::time::Duration;

use perseus::{
ErrorCause, GenericErrorWithCause, RenderFnResult, RenderFnResultWithCause, Template,
};
Expand All @@ -21,7 +23,7 @@ pub fn get_template<G: Html>() -> Template<G> {
Template::new("timeisr")
.template(time_page)
// This page will revalidate every five seconds (to illustrate revalidation)
.revalidate_after("5s".to_string())
.revalidate_after(Duration::new(5, 0))
.incremental_generation()
.build_state_fn(get_build_state)
.build_paths_fn(get_build_paths)
Expand Down
4 changes: 3 additions & 1 deletion examples/showcase/src/templates/time_root.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::time::Duration;

use perseus::{RenderFnResultWithCause, Template};
use serde::{Deserialize, Serialize};
use sycamore::prelude::{component, view, Html, View};
Expand All @@ -20,7 +22,7 @@ pub fn get_template<G: Html>() -> Template<G> {
.template(time_page)
// This page will revalidate every five seconds (to illustrate revalidation)
// Try changing this to a week, even though the below custom logic says to always revalidate, we'll only do it weekly
.revalidate_after("5s".to_string())
.revalidate_after(Duration::new(5, 0))
.should_revalidate_fn(|| async { Ok(true) })
.build_state_fn(get_build_state)
}
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::templates::TemplateMap;
use crate::translations_manager::TranslationsManager;
use crate::translator::Translator;
use crate::{
decode_time_str::decode_time_str,
stores::{ImmutableStore, MutableStore},
Template,
};
Expand Down Expand Up @@ -166,7 +165,10 @@ async fn gen_state_for_path(
// Handle revalidation, we need to parse any given time strings into datetimes
// 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 means we don't have to rebuild every locale simultaneously
Expand Down
67 changes: 16 additions & 51 deletions packages/perseus/src/decode_time_str.rs
Original file line number Diff line number Diff line change
@@ -1,55 +1,20 @@
use crate::errors::*;
use chrono::{Duration, Utc};
use chrono::Utc;
use std::time;

// 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:
///
/// - 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(),
})
}
};
duration_after_current = duration_after_current + duration;
// Reset that working variable
curr_duration_length = String::new();
}
/// Represents a duration that can be computed relative to the current time.
#[derive(Debug, Clone)]
pub struct ComputedDuration(chrono::Duration);

impl ComputedDuration {
pub fn new<I: Into<time::Duration>>(duration: I) -> Self {
let duration = chrono::Duration::from_std(duration.into()).unwrap();
Self(duration)
}
// 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())
/// 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()
}
}
12 changes: 8 additions & 4 deletions packages/perseus/src/server/render.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
use crate::decode_time_str::decode_time_str;
use crate::errors::*;
use crate::page_data::PageData;
use crate::stores::{ImmutableStore, MutableStore};
Expand Down Expand Up @@ -168,7 +167,10 @@ async fn revalidate(
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())?;
let datetime_to_revalidate = template
.get_revalidate_interval()
.unwrap()
.compute_timestamp();
mutable_store
.write(
&format!("static/{}.revld.txt", path_encoded),
Expand Down Expand Up @@ -281,8 +283,10 @@ pub async fn get_page_for_template(
// We don't need to worry about revalidation that operates by logic, that's 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
mutable_store
Expand Down
17 changes: 9 additions & 8 deletions packages/perseus/src/template.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// This file contains logic to define how templates are rendered

use crate::decode_time_str::ComputedDuration;
use crate::default_headers::default_headers;
use crate::errors::*;
use crate::translator::Translator;
Expand All @@ -12,6 +13,7 @@ use std::collections::HashMap;
use std::pin::Pin;
use std::rc::Rc;
use std::sync::Arc;
use std::time::Duration;
use sycamore::context::{ContextProvider, ContextProviderProps};
use sycamore::prelude::{view, View};

Expand Down Expand Up @@ -194,12 +196,12 @@ pub struct Template<G: Html> {
/// to revalidation after a time in NextJS, with the improvement of custom logic. If used with `revalidate_after`, this function will
/// only be run after that time period. This function will not be parsed anything specific to the request that invoked it.
should_revalidate: Option<ShouldRevalidateFn>,
/// A length of time after which to prerender the template again. This is equivalent to revalidating in NextJS. 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.
/// A length of time after which to prerender the template again. This is equivalent to revalidating in NextJS.
/// This will be converted into a datetime to wait for, which will be updated after every 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`).
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 generated, request state will be prioritized.
amalgamate_states: Option<AmalgamateStatesFn>,
Expand Down Expand Up @@ -395,7 +397,7 @@ impl<G: Html> Template<G> {
self.path.clone()
}
/// Gets the interval after which the template will next revalidate.
pub fn get_revalidate_interval(&self) -> Option<String> {
pub fn get_revalidate_interval(&self) -> Option<ComputedDuration> {
self.revalidate_after.clone()
}

Expand Down Expand Up @@ -541,14 +543,13 @@ impl<G: Html> Template<G> {
}
self
}
/// Enables the *revalidation* strategy (time variant). This takes a time string of a form like `1w` for one week. More details are available
/// [in the book](https://arctic-hen7.github.io/perseus/strategies/revalidation.html#time-syntax).
/// Enables the *revalidation* strategy (time variant).
#[allow(unused_mut)]
#[allow(unused_variables)]
pub fn revalidate_after(mut self, val: String) -> Template<G> {
pub fn revalidate_after<I: Into<Duration>>(mut self, val: I) -> Template<G> {
#[cfg(feature = "server-side")]
{
self.revalidate_after = Some(val);
self.revalidate_after = Some(ComputedDuration::new(val));
}
self
}
Expand Down

0 comments on commit 06e8d55

Please sign in to comment.