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
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
use super::Turbine;
use crate::{
    errors::*,
    i18n::{TranslationsManager, Translator},
    init::PerseusAppBase,
    path::*,
    plugins::PluginAction,
    reactor::{RenderMode, RenderStatus},
    router::{match_route, FullRouteVerdict},
    server::get_path_slice,
    state::{BuildPaths, StateGeneratorInfo, TemplateState},
    stores::MutableStore,
    template::Entity,
    utils::{minify, ssr_fallible},
};
use futures::{
    future::{try_join_all, BoxFuture},
    FutureExt,
};
use std::{cell::RefCell, collections::HashMap, rc::Rc, sync::Arc};
use sycamore::web::SsrNode;

impl<M: MutableStore, T: TranslationsManager> Turbine<M, T> {
    /// Builds your whole app for being run on a server. Do not use this
    /// function if you want to export your app.
    ///
    /// This returns an `Arc<Error>`, since any errors are passed to plugin
    /// actions for further processing.
    pub async fn build(&mut self) -> Result<(), Arc<Error>> {
        self.plugins
            .functional_actions
            .build_actions
            .before_build
            .run((), self.plugins.get_plugin_data())
            .map_err(|err| Arc::new(err.into()))?;
        let res = self.build_internal(false).await;
        if let Err(err) = res {
            let err: Arc<Error> = Arc::new(err.into());
            self.plugins
                .functional_actions
                .build_actions
                .after_failed_build
                .run(err.clone(), self.plugins.get_plugin_data())
                .map_err(|err| Arc::new(err.into()))?;

            Err(err)
        } else {
            self.plugins
                .functional_actions
                .build_actions
                .after_successful_build
                .run((), self.plugins.get_plugin_data())
                .map_err(|err| Arc::new(err.into()))?;

            Ok(())
        }
    }

    pub(super) async fn build_internal(&mut self, exporting: bool) -> Result<(), ServerError> {
        // Build the global state (also adds it to the immutable store)
        self.global_state = self.build_global_state(exporting).await?;

        let mut render_cfg = HashMap::new();

        // Now build every capsule's state in parallel (capsules are never rendered
        // outside a page)
        let mut capsule_futs = Vec::new();
        for capsule in self.entities.values() {
            if capsule.is_capsule {
                capsule_futs.push(self.build_template_or_capsule(capsule, exporting));
            }
        }
        let capsule_render_cfg_frags = try_join_all(capsule_futs).await?;
        // Add to the render config as appropriate
        for fragment in capsule_render_cfg_frags.into_iter() {
            render_cfg.extend(fragment.into_iter());
        }

        // We now update the render config with everything we learned from the capsule
        // building so the actual renders of the pages can resolve the widgets
        // they have (if they have one that's not in here, it wasn't even built,
        // and would cancel the render).
        self.render_cfg = render_cfg.clone();

        // Now build every template's state in parallel
        let mut template_futs = Vec::new();
        for template in self.entities.values() {
            if !template.is_capsule {
                template_futs.push(self.build_template_or_capsule(template, exporting));
            }
        }
        let template_render_cfg_frags = try_join_all(template_futs).await?;
        // Add to the render config as appropriate
        for fragment in template_render_cfg_frags.into_iter() {
            render_cfg.extend(fragment.into_iter());
        }

        // Now write the render config to the immutable store
        self.immutable_store
            .write(
                "render_conf.json",
                &serde_json::to_string(&render_cfg).unwrap(),
            )
            .await?;
        self.render_cfg = render_cfg;

        // And build the HTML shell (so that this does the exact same thing as
        // instantiating from files)
        let html_shell = PerseusAppBase::<SsrNode, M, T>::get_html_shell(
            self.index_view_str.to_string(),
            &self.root_id,
            &self.render_cfg,
            &self.plugins,
        )
        .await?;
        self.html_shell = Some(html_shell);

        Ok(())
    }
    /// Builds the global state, returning the state generated. This will also
    /// write the global state to the immutable store (there is no such
    /// thing as revalidation for global state, and it is *extremely*
    /// unlikely that there ever will be).
    async fn build_global_state(&self, exporting: bool) -> Result<TemplateState, ServerError> {
        let gsc = &self.global_state_creator;

        if exporting && (gsc.uses_request_state() || gsc.can_amalgamate_states()) {
            return Err(ExportError::GlobalStateNotExportable.into());
        }

        let global_state = if gsc.uses_build_state() {
            // Generate the global state and write it to a file
            let global_state = gsc.get_build_state().await?;
            self.immutable_store
                .write(
                    // We put the locale at the end to prevent confusion with any pages
                    "static/global_state.json",
                    &global_state.state.to_string(),
                )
                .await?;
            global_state
        } else {
            // If there's no build-time handler, we'll give an empty state. This will
            // be very unexpected if the user is generating at request-time, since all
            // the pages they have at build-time will be unable to access the global state.
            // We could either completely disable build-time rendering when there's
            // request-time global state generation, or we could give the user smart
            // errors and let them manage this problem themselves by gating their
            // usage of global state at build-time, since `.try_get_global_state()`
            // will give a clear `Ok(None)` at build-time. For speed, the latter
            // approach has been chosen.
            //
            // This is one of the biggest 'gotchas' in Perseus, and is clearly documented!
            TemplateState::empty()
        };
        Ok(global_state)
    }
    /// This returns the fragment of the render configuration generated by this
    /// template/capsule, including the additions of any extra widgets that
    /// needed to be incrementally built ahead of time.
    // Note we use page/template rhetoric here, but this could equally be
    // widget/capsule
    async fn build_template_or_capsule(
        &self,
        entity: &Entity<SsrNode>,
        exporting: bool,
    ) -> Result<HashMap<String, String>, ServerError> {
        // If we're exporting, ensure that all the capsule's strategies are export-safe
        // (not requiring a server)
        if exporting
            && (entity.revalidates() ||
                entity.uses_incremental() ||
                entity.uses_request_state() ||
                // We check amalgamation as well because it involves request state, even if that wasn't provided
                entity.can_amalgamate_states())
        {
            return Err(ExportError::TemplateNotExportable {
                template_name: entity.get_path(),
            }
            .into());
        }

        let mut render_cfg_frag = HashMap::new();

        // We extract the paths and extra state for rendering outside, but we handle the
        // render config inside this block
        let (paths, extra) = if entity.uses_build_paths() {
            let BuildPaths { mut paths, extra } = entity.get_build_paths().await?;

            // Add all the paths to the render config (stripping erroneous slashes as we go)
            for mut page_path in paths.iter_mut() {
                // Strip any erroneous slashes
                let stripped = page_path.strip_prefix('/').unwrap_or(page_path);
                let mut stripped = stripped.to_string();
                page_path = &mut stripped;

                let full_path = format!("{}/{}", &entity.get_path(), &page_path);
                // And perform another strip for index pages to work
                let full_path = full_path.strip_suffix('/').unwrap_or(&full_path);
                let full_path = full_path.strip_prefix('/').unwrap_or(full_path);
                render_cfg_frag.insert(full_path.to_string(), entity.get_path());
            }

            // Now if the page uses ISR, add an explicit `/*` in there after the template
            // root path. Incremental rendering requires build-time path generation.
            if entity.uses_incremental() {
                render_cfg_frag.insert(format!("{}/*", &entity.get_path()), entity.get_path());
            }

            (paths, extra)
        } else {
            // There's no facility to generate extra paths for this template, so it only
            // renders itself

            // The render config should map the only page this generates to the template of
            // the same name
            render_cfg_frag.insert(entity.get_path(), entity.get_path());
            // No extra state, one empty path for the index
            (vec![String::new()], TemplateState::empty())
        };
        // We write the extra state even if it's empty
        self.immutable_store
            .write(
                &format!(
                    "static/{}.extra.json",
                    urlencoding::encode(&entity.get_path())
                ),
                &extra.state.to_string(),
            )
            .await?;

        // We now have a populated render config, so we should build each path in
        // parallel for each locale, if we can. Yes, the function we're calling
        // will also write the revalidation text, but, if you're not using build
        // state or being basic, the you're using request state, which means
        // revalidation is completely irrelevant, since you're revalidating on
        // every load.
        if entity.uses_build_state() || entity.is_basic() {
            let mut path_futs = Vec::new();
            for path in paths.into_iter() {
                for locale in self.locales.get_all() {
                    let path = PurePath(path.clone());
                    path_futs.push(self.build_path_or_widget_for_locale(
                        path,
                        entity,
                        &extra,
                        locale,
                        self.global_state.clone(),
                        exporting,
                        false,
                    ));
                }
            }
            // Extend the render configuration with any incrementally generated widgets
            let render_cfg_exts = try_join_all(path_futs).await?;
            for ext in render_cfg_exts {
                render_cfg_frag.extend(ext.into_iter());
            }
        }

        Ok(render_cfg_frag)
    }
    /// The path this accepts is the path *within* the entity, not including the
    /// entity's name! It is assumed that the path provided to this function
    /// has been stripped of extra leading/trailing forward slashes. This
    /// returns a map of widgets that needed to be incrementally built ahead
    /// of time to avoid rescheduling that should be added to the render
    /// config.
    ///
    /// This function will do nothing for entities that are not either basic or
    /// build-state-generating.
    ///
    /// This function is `super`-public because it's used to generate
    /// incremental pages. Because of this, it also takes in the most
    /// up-to-date global state.
    #[allow(clippy::too_many_arguments)] // Internal function
    pub(super) async fn build_path_or_widget_for_locale(
        &self,
        path: PurePath,
        entity: &Entity<SsrNode>,
        extra: &TemplateState,
        locale: &str,
        global_state: TemplateState,
        exporting: bool,
        // This is used in request-time incremental generation
        force_mutable: bool,
    ) -> Result<HashMap<String, String>, ServerError> {
        let translator = self
            .translations_manager
            .get_translator_for_locale(locale.to_string())
            .await?;

        let full_path_without_locale = PathWithoutLocale(match entity.uses_build_paths() {
            // Note the stripping of trailing `/`s here (otherwise index build paths fail)
            true => {
                let full = format!("{}/{}", &entity.get_path(), path.0);
                let full = full.strip_suffix('/').unwrap_or(&full);
                full.strip_prefix('/').unwrap_or(full).to_string()
            }
            // We don't want to concatenate the name twice if we don't have to
            false => entity.get_path(),
        });
        // Create the encoded path, which always includes the locale (even if it's
        // `xx-XX` in a non-i18n app)
        //
        // BUG: insanely nested paths won't work whatsoever if the filename is too long,
        // maybe hash instead?
        let full_path_encoded = format!(
            "{}-{}",
            translator.get_locale(),
            urlencoding::encode(&full_path_without_locale)
        );
        // And we'll need the full path with the locale for the `PageProps`
        // If it's `xx-XX`, we should just have it without the locale (this may be
        // interacted with by users)
        let locale = translator.get_locale();
        let full_path = PathMaybeWithLocale::new(&full_path_without_locale, &locale);

        // First, if this page revalidates, write a timestamp about when it was built to
        // the mutable store (this will be updated to keep track)
        if entity.revalidates_with_time() {
            let datetime_to_revalidate = entity
                .get_revalidate_interval()
                .unwrap()
                .compute_timestamp();
            // Note that different locales do have different revalidation schedules
            self.mutable_store
                .write(
                    &format!("static/{}.revld.txt", full_path_encoded),
                    &datetime_to_revalidate.to_string(),
                )
                .await?;
        }

        let state = if entity.is_basic() {
            // We don't bother writing the state of basic entities
            TemplateState::empty()
        } else if entity.uses_build_state() {
            let build_state = entity
                .get_build_state(StateGeneratorInfo {
                    // IMPORTANT: It is very easy to break Perseus here; always make sure this is
                    // the pure path, without the template name!
                    // TODO Compat mode for v0.3.0x?
                    path: (*path).clone(),
                    locale: translator.get_locale(),
                    extra: extra.clone(),
                })
                .await?;
            // Write the state to the appropriate store (mutable if the entity revalidates)
            let state_str = build_state.state.to_string();
            if force_mutable || entity.revalidates() {
                self.mutable_store
                    .write(&format!("static/{}.json", full_path_encoded), &state_str)
                    .await?;
            } else {
                self.immutable_store
                    .write(&format!("static/{}.json", full_path_encoded), &state_str)
                    .await?;
            }

            build_state
        } else {
            // There's nothing we can do with any other sort of template at build-time
            return Ok(HashMap::new());
        };

        // For templates (*not* capsules), we'll render the full content (with
        // dependencies), and the head (which capsules don't have), provided
        // it's not always going to be useless (i.e. if this uses request state)
        if !entity.is_capsule && !entity.uses_request_state() {
            // Render the head (which has no dependencies)
            let head_str =
                entity.render_head_str(state.clone(), global_state.clone(), &translator)?;
            let head_str = minify(&head_str, true)?;
            if force_mutable || entity.revalidates() {
                self.mutable_store
                    .write(
                        &format!("static/{}.head.html", full_path_encoded),
                        &head_str,
                    )
                    .await?;
            } else {
                self.immutable_store
                    .write(
                        &format!("static/{}.head.html", full_path_encoded),
                        &head_str,
                    )
                    .await?;
            }

            // This stores a list of widgets that are able to be incrementally generated,
            // but that weren't in the build paths listing of their capsules.
            // These were, however, used at build-time by another page, meaning
            // they should be automatically built for convenience --- we can
            // therefore avoid an unnecessary build reschedule.
            //
            // Note that incremental generation is entirely side-effect based, so this
            // function call just internally maintains a series of additions to
            // the render configuration to be added properly once we're out of
            // all the `async`. This *could* lead to race conditions on the
            // immutable store, but Tokio should handle this.
            self.build_render(
                entity,
                full_path,
                &full_path_encoded,
                translator,
                state,
                global_state,
                exporting,
                force_mutable,
                // Start off with what's already known
                self.render_cfg.clone(),
            )
            .await
        } else {
            Ok(HashMap::new())
        }
    }

    /// This enables recursion for rendering incrementally rendered widgets
    /// (ideally, users would include the widgets they want at build-time in
    /// their build paths, but, if we don't do this, then unnecessary
    /// build reschedulings would abound a little too much).
    ///
    /// Each iteration of this will return a map to extend the render
    /// configuration with, but the function maintains its own internal
    /// render configuration passed through arguments, because it's designed to
    /// be part of the larger asynchronous build process, therefore modifying
    /// the root-level render config is not safe until the end.
    #[allow(clippy::too_many_arguments)] // Internal function
    fn build_render<'a>(
        &'a self,
        entity: &'a Entity<SsrNode>,
        full_path: PathMaybeWithLocale,
        full_path_encoded: &'a str,
        translator: Translator,
        state: TemplateState,
        global_state: TemplateState,
        exporting: bool,
        force_mutable: bool,
        mut render_cfg: HashMap<String, String>,
    ) -> BoxFuture<'a, Result<HashMap<String, String>, ServerError>> {
        async move {
            let (prerendered, render_status, widget_states, paps) = {
                // Construct the render mode we're using, which is needed because we don't
                // know what dependencies are in a page/widget until we actually render it,
                // which means we might find some that can't be built at build-time.
                let render_status = Rc::new(RefCell::new(RenderStatus::Ok));
                let widget_states = Rc::new(RefCell::new(HashMap::new()));
                let possibly_incremental_paths = Rc::new(RefCell::new(Vec::new()));
                let mode = RenderMode::Build {
                    render_status: render_status.clone(),
                    // Make sure what we have is passed through
                    widget_render_cfg: render_cfg.clone(),
                    immutable_store: self.immutable_store.clone(),
                    widget_states: widget_states.clone(),
                    possibly_incremental_paths: possibly_incremental_paths.clone(),
                };

                // Now prerender the actual content
                let prerendered = ssr_fallible(|cx| {
                    entity.render_for_template_server(
                        full_path.clone(),
                        state.clone(),
                        global_state.clone(),
                        mode.clone(),
                        cx,
                        &translator,
                    )
                })?;
                let render_status = render_status.take();

                // With the prerender over, all references to this have been dropped
                // TODO Avoid cloning everything here
                let widget_states = (*widget_states).clone().into_inner();
                // let widget_states = Rc::try_unwrap(widget_states).unwrap().into_inner();
                // We know this is a `HashMap<String, (String, Value)>`, which will work
                let widget_states = serde_json::to_string(&widget_states).unwrap();
                let paps = (*possibly_incremental_paths).clone().into_inner();

                (prerendered, render_status, widget_states, paps)
            };

            // Check how the render went
            match render_status {
                RenderStatus::Ok => {
                    // `Ok` does not necessarily mean all is well: anything in `possibly_incremental_paths`
                    // constitutes a widget that could not be rendered because it wasn't in the
                    // render config (either needs to be incrementally rendered, or it doesn't exist).
                    if paps.is_empty() {
                        let prerendered = minify(&prerendered, true)?;
                        // Write that prerendered HTML to a static file (whose presence is used to
                        // indicate that this page/widget was fine to be built at
                        // build-time, and will not change at request-time;
                        // therefore this will be blindly returned at request-time).
                        // We also write a JSON file with a map of all the widget states, since the
                        // browser will need to know them for hydration.
                        if force_mutable || entity.revalidates() {
                            self.mutable_store
                                .write(&format!("static/{}.html", full_path_encoded), &prerendered)
                                .await?;
                            self.mutable_store
                                .write(
                                    &format!("static/{}.widgets.json", full_path_encoded),
                                    &widget_states,
                                )
                                .await?;
                        } else {
                            self.immutable_store
                                .write(&format!("static/{}.html", full_path_encoded), &prerendered)
                                .await?;
                            self.immutable_store
                                .write(
                                    &format!("static/{}.widgets.json", full_path_encoded),
                                    &widget_states,
                                )
                                .await?;
                        }
                        // In this path, we haven't accumulated any extensions to the render config, and we can jsut return
                        // whatever we were given (thereby preserving the effect of previous recursions)
                        Ok(render_cfg)
                    } else {
                        let mut futs = Vec::new();
                        for path in paps {
                            let locale = translator.get_locale();
                            let render_cfg = render_cfg.clone();
                            let global_state = global_state.clone();
                            futs.push(async move {
                                // It's also possible that these widgets just don't exist, so check that
                                let localized_path = PathMaybeWithLocale::new(&path, &locale);
                                let path_slice = get_path_slice(&localized_path);
                                let verdict = match_route(
                                    &path_slice,
                                    &render_cfg,
                                    &self.entities,
                                    &self.locales,
                                );

                                match verdict.into_full(&self.entities) {
                                    FullRouteVerdict::Found(route_info) => {
                                        let capsule_name = route_info.entity.get_path();
                                        // This will always exist
                                        let capsule_extra = match self
                                            .immutable_store
                                            .read(&format!(
                                                "static/{}.extra.json",
                                                urlencoding::encode(&capsule_name)
                                            ))
                                            .await
                                        {
                                            Ok(state) => {
                                                TemplateState::from_str(&state).map_err(|err| ServerError::InvalidBuildExtra {
                                                    template_name: capsule_name.clone(),
                                                    source: err,
                                                })?
                                            }
                                            // If this happens, then the immutable store has been tampered with, since
                                            // the build logic generates some kind of state for everything
                                            Err(_) => {
                                                return Err(ServerError::MissingBuildExtra {
                                                    template_name: capsule_name,
                                                })
                                            }
                                        };

                                        // The `path` is a `PathWithoutLocale`, and we need to strip the capsule name.
                                        // Because this is produced from the widget component in the first place, it should
                                        // be perfectly safe to unwrap everything here (any failures indicate users hand-rolling
                                        // widgets or a Perseus bug).
                                        let pure_path = path
                                            .strip_prefix(&capsule_name)
                                            .expect("couldn't strip capsule name from widget (unless you're hand-rolling widgets, this is a Perseus bug)");
                                        let pure_path = pure_path.strip_prefix('/').unwrap_or(pure_path);
                                        let pure_path = PurePath(pure_path.to_string());

                                        // This will perform the same process as we've done, recursing as necessary (yes, double recursion),
                                        // and it will return a set of render configuration extenstions too. This does NOT use the render
                                        // configuration internally, so we can avoid infinite loops.
                                        let exts = self.build_path_or_widget_for_locale(
                                            pure_path,
                                            route_info.entity,
                                            &capsule_extra,
                                            &locale,
                                            global_state.clone(),
                                            exporting,
                                            // It's incremental generation, but this will *end up* acting
                                            // like it was in build paths all along, so don't force the mutable
                                            // store unless we're being asked to from a higher level
                                            force_mutable,
                                        )
                                                       .await?;

                                        let mut render_cfg_ext = HashMap::new();

                                        render_cfg_ext.extend(exts.into_iter());
                                        // Now add this actual capsule itself
                                        render_cfg_ext.insert(path.0, capsule_name);

                                        Ok(render_cfg_ext)
                                    }
                                    FullRouteVerdict::LocaleDetection(_) => {
                                        Err(ServerError::ResolveDepLocaleRedirection {
                                            locale: locale.to_string(),
                                            widget: path.to_string(),
                                        })
                                    }
                                    FullRouteVerdict::NotFound { .. } => Err(ServerError::ResolveDepNotFound { widget: path.to_string(), locale: locale.to_string() }),
                                }
                            });
                        }
                        let render_cfg_exts = try_join_all(futs).await?;
                        // Add all those extensions to our internal copy, which will be used for recursion
                        for ext in render_cfg_exts {
                            render_cfg.extend(ext.into_iter());
                        }


                        // We've rendered all the possibly incremental widgets, failing if any were actually just nonexistent.
                        // However, because building a widget means building its state, not prerendering it, we don't know
                        // if we're going to have to do all this again because one of the widgets has yet another incremental
                        // dependency. So, restart the whole render process!
                        self.build_render(entity, full_path, full_path_encoded, translator, state, global_state, exporting, force_mutable, render_cfg).await
                    }
                }
                RenderStatus::Err(err) => Err(err),
                // One of the dependencies couldn't be built at build-time,
                // so, by not writing a prerender to the store, we implicitly
                // reschedule it (unless this hasn't been allowed by the user,
                // or if we're exporting).
                //
                // Important: this will **not** be returned for pages including
                // incremental widgets that haven't been built yet, those are handled
                // through `Ok`. Potentially non-existent widgets will also be handled
                // through there.
                RenderStatus::Cancelled => {
                    if exporting {
                        Err(ExportError::DependenciesNotExportable {
                            template_name: entity.get_path(),
                        }
                                   .into())
                    } else if !entity.can_be_rescheduled {
                        Err(ServerError::TemplateCannotBeRescheduled {
                            template_name: entity.get_path(),
                        })
                    } else {
                        // It's fine to reschedule later, so just return the widgets we have
                        Ok(render_cfg)
                    }
                }
            }
        }.boxed()
    }
}