Skip to main content

topcoat_runtime/
shard_scope.rs

1use std::{
2    pin::Pin,
3    task::{Context, Poll, ready},
4};
5
6use pin_project_lite::pin_project;
7use topcoat_core::{context::Cx, error::Result, identity::Identity};
8use topcoat_view::{NodeViewParts, PartsWriter, View, ViewFirst, ViewSwap, internal::Builder};
9
10use crate::Js;
11
12pin_project! {
13    /// A shard invocation's content between the markers the browser finds
14    /// it by.
15    ///
16    /// The markers surround the shard body's first content. Later updates
17    /// from a live body pass through unchanged, so a live shard keeps
18    /// streaming like any other live content.
19    pub struct ShardScope<'cx, V> {
20        cx: &'cx Cx,
21        // The identity of the shard invocation, which names the scope in
22        // its markers and which the browser sends back with a re-render
23        // request so the shard body derives the same identities as the
24        // inline render.
25        identity: Identity,
26        // The request URL, with route groups removed and `/` for the root.
27        url: &'static str,
28        exprs: Vec<Js>,
29        #[pin]
30        view: V,
31    }
32}
33
34impl<'cx, V> ShardScope<'cx, V> {
35    #[inline]
36    #[must_use]
37    pub fn new(
38        cx: &'cx Cx,
39        identity: Identity,
40        url: &'static str,
41        exprs: Vec<Js>,
42        view: V,
43    ) -> Self {
44        Self {
45            cx,
46            identity,
47            url,
48            exprs,
49            view,
50        }
51    }
52}
53
54impl<V: View> View for ShardScope<'_, V> {
55    fn poll_first(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<ViewFirst>> {
56        let this = self.project();
57        let first = ready!(this.view.poll_first(cx))?;
58        let identity = serde_json::to_string(&this.identity.to_string()).unwrap();
59        let content = Builder::block(this.cx, |builder| {
60            builder.node(StartMarker {
61                url: this.url,
62                identity: &identity,
63                exprs: this.exprs,
64            });
65            builder.view(first.content);
66            builder.node(EndMarker { identity });
67        });
68        Poll::Ready(Ok(ViewFirst {
69            content,
70            live: first.live,
71        }))
72    }
73
74    fn poll_swap(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<Option<ViewSwap>>> {
75        self.project().view.poll_swap(cx)
76    }
77}
78
79/// The comment opening a shard's content.
80///
81/// The browser runtime posts re-render requests to the URL. The identity
82/// is stable across renders, so a re-render of the enclosing content
83/// produces the same markers and the browser can keep them. Each
84/// parameter's JavaScript source is wrapped in a quoted string. The source
85/// parts are sealed with the comment context, so any `"` inside the source
86/// renders as `&quot;` and the quotes stay unambiguous delimiters on the
87/// client.
88struct StartMarker<'a> {
89    url: &'static str,
90    /// The identity as a JSON string, quotes included.
91    identity: &'a str,
92    exprs: &'a [Js],
93}
94
95impl NodeViewParts for StartMarker<'_> {
96    fn into_view_parts(self, _cx: &Cx, parts: &mut PartsWriter<'_>) {
97        // <!-- ::topcoat::shard::start("<url>", "<identity>", ["<js>", ...]) -->
98        parts.push_comment(|comment| {
99            comment
100                .push_promoted_str_unescaped(&"::topcoat::shard::start(")
101                .push_string_unescaped(serde_json::to_string(self.url).unwrap())
102                .push_promoted_str_unescaped(&", ")
103                .push_string_unescaped(self.identity.to_owned())
104                .push_promoted_str_unescaped(&", [");
105            let last = self.exprs.len().saturating_sub(1);
106            for (index, expr) in self.exprs.iter().enumerate() {
107                comment.push_promoted_str_unescaped(&"\"");
108                expr.write(comment);
109                comment.push_promoted_str_unescaped(&"\"");
110                if index != last {
111                    comment.push_promoted_str_unescaped(&", ");
112                }
113            }
114            comment.push_promoted_str_unescaped(&"])");
115        });
116    }
117}
118
119/// The comment closing a shard's content.
120struct EndMarker {
121    /// The identity as a JSON string, quotes included.
122    identity: String,
123}
124
125impl NodeViewParts for EndMarker {
126    fn into_view_parts(self, _cx: &Cx, parts: &mut PartsWriter<'_>) {
127        // <!-- ::topcoat::shard::end("<identity>") -->
128        parts.push_comment(|comment| {
129            comment
130                .push_promoted_str_unescaped(&"::topcoat::shard::end(")
131                .push_string_unescaped(self.identity)
132                .push_promoted_str_unescaped(&")");
133        });
134    }
135}