1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
use crate::decode_time_str::decode_time_str;
use crate::errors::*;
use crate::page_data::PageData;
use crate::stores::{ImmutableStore, MutableStore};
use crate::template::{States, Template, TemplateMap};
use crate::translations_manager::TranslationsManager;
use crate::translator::Translator;
use crate::Request;
use chrono::{DateTime, Utc};
use sycamore::prelude::SsrNode;

// Note: the reason there are a ton of seemingly useless named lifetimes here is because of [this](https://github.com/rust-lang/rust/issues/63033)

/// Renders a template that uses state generated at build-time. This can't be used for pages that revalidate because their data are
/// stored in a mutable store.
async fn render_build_state(
    path_encoded: &str,
    immutable_store: &ImmutableStore,
) -> Result<(String, String, Option<String>), ServerError> {
    // Get the static HTML
    let html = immutable_store
        .read(&format!("static/{}.html", path_encoded))
        .await?;
    let head = immutable_store
        .read(&format!("static/{}.head.html", path_encoded))
        .await?;
    // Get the static JSON
    let state = match immutable_store
        .read(&format!("static/{}.json", path_encoded))
        .await
    {
        Ok(state) => Some(state),
        Err(_) => None,
    };

    Ok((html, head, state))
}
/// Renders a template that uses state generated at build-time. This is specifically for page that revalidate, because they store data
/// in the mutable store.
async fn render_build_state_for_mutable(
    path_encoded: &str,
    mutable_store: &impl MutableStore,
) -> Result<(String, String, Option<String>), ServerError> {
    // Get the static HTML
    let html = mutable_store
        .read(&format!("static/{}.html", path_encoded))
        .await?;
    let head = mutable_store
        .read(&format!("static/{}.head.html", path_encoded))
        .await?;
    // Get the static JSON
    let state = match mutable_store
        .read(&format!("static/{}.json", path_encoded))
        .await
    {
        Ok(state) => Some(state),
        Err(_) => None,
    };

    Ok((html, head, state))
}
/// Renders a template that generated its state at request-time. Note that revalidation and incremental generation have no impact on
/// SSR-rendered pages. This does everything at request-time, and so doesn't need a mutable or immutable store.
async fn render_request_state(
    template: &Template<SsrNode>,
    translator: &Translator,
    path: &str,
    req: Request,
) -> Result<(String, String, Option<String>), ServerError> {
    // Generate the initial state (this may generate an error, but there's no file that can't exist)
    let state = Some(
        template
            .get_request_state(path.to_string(), translator.get_locale(), req)
            .await?,
    );
    // Use that to render the static HTML
    let html = sycamore::render_to_string(|| {
        template.render_for_template(state.clone(), translator, true)
    });
    let head = template.render_head_str(state.clone(), translator);

    Ok((html, head, state))
}
/// Checks if a template that uses incremental generation has already been cached. If the template was prerendered by *build paths*,
/// then it will have already been matched because those are declared verbatim in the render configuration. Therefore, this function
/// only searches for pages that have been cached later, which means it needs a mutable store.
async fn get_incremental_cached(
    path_encoded: &str,
    mutable_store: &impl MutableStore,
) -> Option<(String, String)> {
    let html_res = mutable_store
        .read(&format!("static/{}.html", path_encoded))
        .await;

    // We should only treat it as cached if it can be accessed and if we aren't in development (when everything should constantly reload)
    match html_res {
        Ok(html) if !cfg!(debug_assertions) => {
            // If the HTML exists, the head must as well
            let head = mutable_store
                .read(&format!("static/{}.head.html", path_encoded))
                .await
                .unwrap();
            Some((html, head))
        }
        Ok(_) | Err(_) => None,
    }
}
/// Checks if a template should revalidate by time. All revalidation timestamps are stored in a mutable store, so that's what this
/// function uses.
async fn should_revalidate(
    template: &Template<SsrNode>,
    path_encoded: &str,
    mutable_store: &impl MutableStore,
) -> Result<bool, ServerError> {
    let mut should_revalidate = false;
    // If it revalidates after a certain period of time, we needd to check that BEFORE the custom logic
    if template.revalidates_with_time() {
        // Get the time when it should revalidate (RFC 3339)
        // This will be updated, so it's in a mutable store
        let datetime_to_revalidate_str = mutable_store
            .read(&format!("static/{}.revld.txt", path_encoded))
            .await?;
        let datetime_to_revalidate = DateTime::parse_from_rfc3339(&datetime_to_revalidate_str)
            .map_err(|e| {
                let serve_err: ServeError = e.into();
                serve_err
            })?;
        // Get the current time (UTC)
        let now = Utc::now();

        // If the datetime to revalidate is still in the future, end with `false`
        if datetime_to_revalidate > now {
            return Ok(false);
        }
        should_revalidate = true;
    }

    // Now run the user's custom revalidation logic
    if template.revalidates_with_logic() {
        should_revalidate = template.should_revalidate().await?;
    }
    Ok(should_revalidate)
}
/// Revalidates a template. All information about templates that revalidate (timestamp, content, head, and state) is stored in a
/// mutable store, so that's what this function uses.
async fn revalidate(
    template: &Template<SsrNode>,
    translator: &Translator,
    path: &str,
    path_encoded: &str,
    mutable_store: &impl MutableStore,
) -> Result<(String, String, Option<String>), ServerError> {
    // We need to regenerate and cache this page for future usage (until the next revalidation)
    let state = Some(
        template
            .get_build_state(
                format!("{}/{}", template.get_path(), path),
                translator.get_locale(),
            )
            .await?,
    );
    let html = sycamore::render_to_string(|| {
        template.render_for_template(state.clone(), translator, true)
    });
    let head = template.render_head_str(state.clone(), translator);
    // 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() {
        // 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())?;
        mutable_store
            .write(
                &format!("static/{}.revld.txt", path_encoded),
                &datetime_to_revalidate,
            )
            .await?;
    }
    mutable_store
        .write(
            &format!("static/{}.json", path_encoded),
            &state.clone().unwrap(),
        )
        .await?;
    mutable_store
        .write(&format!("static/{}.html", path_encoded), &html)
        .await?;
    mutable_store
        .write(&format!("static/{}.head.html", path_encoded), &head)
        .await?;

    Ok((html, head, state))
}

/// Internal logic behind `get_page`. The only differences are that this takes a full template rather than just a template name, which
/// can avoid an unnecessary lookup if you already know the template in full (e.g. initial load server-side routing). Because this
/// handles templates with potentially revalidation and incremental generation, it uses both mutable and immutable stores.
// TODO possible further optimizations on this for futures?
pub async fn get_page_for_template(
    // This must not contain the locale
    raw_path: &str,
    locale: &str,
    template: &Template<SsrNode>,
    // This allows us to differentiate pages for incrementally generated templates that were pre-rendered with build paths (and are in the immutable store) from those generated and cached at runtime (in the mutable store)
    was_incremental_match: bool,
    req: Request,
    (immutable_store, mutable_store): (&ImmutableStore, &impl MutableStore),
    translations_manager: &impl TranslationsManager,
) -> Result<PageData, ServerError> {
    // Get a translator for this locale (for sanity we hope the manager is caching)
    let translator = translations_manager
        .get_translator_for_locale(locale.to_string())
        .await?;

    let mut path = raw_path;
    // If the path is empty, we're looking for the special `index` page
    if path.is_empty() {
        path = "index";
    }
    // Remove `/` from the path by encoding it as a URL (that's what we store) and add the locale
    let path_encoded = format!("{}-{}", locale, urlencoding::encode(path).to_string());

    // Only a single string of HTML is needed, and it will be overridden if necessary (priorities system)
    let mut html = String::new();
    // The same applies for the document metadata
    let mut head = String::new();
    // Multiple rendering strategies may need to amalgamate different states
    let mut states: States = States::new();

    // Handle build state (which might use revalidation or incremental)
    if template.uses_build_state() || template.is_basic() {
        // If the template uses incremental generation, that is its own contained process
        if template.uses_incremental() && was_incremental_match {
            // This template uses incremental generation, and this page was built and cached at runtime in the mutable store
            // Get the cached content if it exists (otherwise `None`)
            let html_and_head_opt = get_incremental_cached(&path_encoded, mutable_store).await;
            match html_and_head_opt {
                // It's cached
                Some((html_val, head_val)) => {
                    // Check if we need to revalidate
                    if should_revalidate(template, &path_encoded, mutable_store).await? {
                        let (html_val, head_val, state) =
                            revalidate(template, &translator, path, &path_encoded, mutable_store)
                                .await?;
                        // Build-time generated HTML is the lowest priority, so we'll only set it if nothing else already has
                        if html.is_empty() {
                            html = html_val;
                            head = head_val;
                        }
                        states.build_state = state;
                    } else {
                        // Build-time generated HTML is the lowest priority, so we'll only set it if nothing else already has
                        if html.is_empty() {
                            html = html_val;
                            head = head_val;
                        }
                        // Get the static JSON (if it exists, but it should)
                        states.build_state = match mutable_store
                            .read(&format!("static/{}.json", path_encoded))
                            .await
                        {
                            Ok(state) => Some(state),
                            Err(_) => None,
                        };
                    }
                }
                // It's not cached
                // All this uses the mutable store because this will be done at runtime
                None => {
                    // We need to generate and cache this page for future usage
                    let state = Some(
                        template
                            .get_build_state(path.to_string(), locale.to_string())
                            .await?,
                    );
                    let html_val = sycamore::render_to_string(|| {
                        template.render_for_template(state.clone(), &translator, true)
                    });
                    let head_val = template.render_head_str(state.clone(), &translator);
                    // 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
                    // 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())?;
                        // 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
                            .write(
                                &format!("static/{}.revld.txt", path_encoded),
                                &datetime_to_revalidate,
                            )
                            .await?;
                    }
                    // Cache all that
                    mutable_store
                        .write(
                            &format!("static/{}.json", path_encoded),
                            &state.clone().unwrap(),
                        )
                        .await?;
                    // Write that prerendered HTML to a static file
                    mutable_store
                        .write(&format!("static/{}.html", path_encoded), &html_val)
                        .await?;
                    mutable_store
                        .write(&format!("static/{}.head.html", path_encoded), &head_val)
                        .await?;

                    states.build_state = state;
                    // Build-time generated HTML is the lowest priority, so we'll only set it if nothing else already has
                    if html.is_empty() {
                        html = html_val;
                        head = head_val;
                    }
                }
            }
        } else {
            // If we're here, incremental generation is either not used or it's irrelevant because the page was rendered in the immutable store at build time

            // Handle if we need to revalidate
            // It'll be in the mutable store if we do
            if should_revalidate(template, &path_encoded, mutable_store).await? {
                let (html_val, head_val, state) =
                    revalidate(template, &translator, path, &path_encoded, mutable_store).await?;
                // Build-time generated HTML is the lowest priority, so we'll only set it if nothing else already has
                if html.is_empty() {
                    html = html_val;
                    head = head_val;
                }
                states.build_state = state;
            } else if template.revalidates() {
                // The template does revalidate, but it doesn't need to revalidate now
                // Nonetheless, its data will be the mutable store
                let (html_val, head_val, state) =
                    render_build_state_for_mutable(&path_encoded, mutable_store).await?;
                // Build-time generated HTML is the lowest priority, so we'll only set it if nothing else already has
                if html.is_empty() {
                    html = html_val;
                    head = head_val;
                }
                states.build_state = state;
            } else {
                // If we don't need to revalidate and this isn't an incrementally generated template, everything is immutable
                let (html_val, head_val, state) =
                    render_build_state(&path_encoded, immutable_store).await?;
                // Build-time generated HTML is the lowest priority, so we'll only set it if nothing else already has
                if html.is_empty() {
                    html = html_val;
                    head = head_val;
                }
                states.build_state = state;
            }
        }
    }
    // Handle request state
    if template.uses_request_state() {
        let (html_val, head_val, state) =
            render_request_state(template, &translator, path, req).await?;
        // Request-time HTML always overrides anything generated at build-time or incrementally (this has more information)
        html = html_val;
        head = head_val;
        states.request_state = state;
    }

    // Amalgamate the states
    // If the user has defined custom logic for this, we'll defer to that
    // Otherwise we go as with HTML, request trumps build
    // Of course, if only one state was defined, we'll just use that regardless (so `None` prioritization is impossible)
    // If this is the case, the build content will still be served, and then it's up to the client to hydrate it with the new amalgamated state
    let state: Option<String>;
    if !states.both_defined() {
        state = states.get_defined()?;
    } else if template.can_amalgamate_states() {
        state = template.amalgamate_states(states)?;
    } else {
        state = states.request_state;
    }

    // Combine everything into one JSON object
    let res = PageData {
        content: html,
        state,
        head,
    };

    Ok(res)
}

/// Gets the HTML/JSON data for the given page path. This will call SSG/SSR/etc., whatever is needed for that page. Note that HTML
/// generated at request-time will **always** replace anything generated at build-time, incrementally, revalidated, etc.
pub async fn get_page(
    // This must not contain the locale
    raw_path: &str,
    locale: &str,
    (template_name, was_incremental_match): (&str, bool),
    req: Request,
    templates: &TemplateMap<SsrNode>,
    (immutable_store, mutable_store): (&ImmutableStore, &impl MutableStore),
    translations_manager: &impl TranslationsManager,
) -> Result<PageData, ServerError> {
    let mut path = raw_path;
    // If the path is empty, we're looking for the special `index` page
    if path.is_empty() {
        path = "index";
    }
    // Get the template to use
    let template = templates.get(template_name);
    let template = match template {
        Some(template) => template,
        // This shouldn't happen because the client should already have performed checks against the render config, but it's handled anyway
        None => {
            return Err(ServeError::PageNotFound {
                path: path.to_string(),
            }
            .into())
        }
    };

    let res = get_page_for_template(
        raw_path,
        locale,
        template,
        was_incremental_match,
        req,
        (immutable_store, mutable_store),
        translations_manager,
    )
    .await?;
    Ok(res)
}