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
#[cfg(any(client, doc))]
mod error;
mod global_state;
#[cfg(all(feature = "hsr", debug_assertions, any(client, doc)))]
mod hsr;
#[cfg(any(client, doc))]
mod initial_load;
#[cfg(engine)]
mod render_mode;
#[cfg(any(client, doc))]
mod start;
mod state;
#[cfg(any(client, doc))]
mod subsequent_load;
mod widget_state;

#[cfg(any(client, doc))]
pub(crate) use initial_load::InitialView;
#[cfg(engine)]
pub(crate) use render_mode::{RenderMode, RenderStatus};

// --- Common imports ---
#[cfg(any(client, doc))]
use crate::template::{BrowserNodeType, EntityMap};
use crate::{
    i18n::Translator,
    state::{GlobalState, GlobalStateType, PageStateStore, TemplateState},
};
use sycamore::{
    prelude::{provide_context, use_context, Scope},
    web::Html,
};

// --- Engine-side imports ---

// --- Browser-side imports ---
#[cfg(any(client, doc))]
use crate::{
    error_views::ErrorViews,
    errors::ClientError,
    errors::ClientInvariantError,
    i18n::{ClientTranslationsManager, Locales, TranslationsManager},
    init::PerseusAppBase,
    plugins::PluginAction,
    router::RouterState,
    state::{FrozenApp, ThawPrefs},
    stores::MutableStore,
};
#[cfg(any(client, doc))]
use serde::{de::DeserializeOwned, Serialize};
#[cfg(any(client, doc))]
use serde_json::Value;
#[cfg(any(client, doc))]
use std::{
    cell::{Cell, RefCell},
    collections::HashMap,
    rc::Rc,
};
#[cfg(any(client, doc))]
use sycamore::{
    reactive::{create_rc_signal, RcSignal},
    view::View,
};

/// The core of Perseus' browser-side systems. This forms a central point for
/// all the Perseus state and rendering logic to operate from. In your own code,
/// this will always be available in the Sycamore context system.
///
/// Note that this is also used on the engine-side for rendering.
#[derive(Debug)]
pub struct Reactor<G: Html> {
    /// The state store, which is used to hold all reactive states, along with
    /// preloads.
    pub(crate) state_store: PageStateStore,
    /// The router state.
    #[cfg(any(client, doc))]
    pub router_state: RouterState,
    /// The user-provided global state, stored with similar mechanics to the
    /// state store, although optimised.
    global_state: GlobalState,

    // --- Browser-side only ---
    /// A previous state the app was once in, still serialized. This will be
    /// rehydrated gradually by the template closures.
    ///
    /// The `bool` in here will be set to `true` if this was created through
    /// HSR, which has slightly more lenient thawing procedures to allow for
    /// data model changes.
    #[cfg(any(client, doc))]
    frozen_app: Rc<RefCell<Option<(FrozenApp, ThawPrefs, bool)>>>,
    /// Whether or not this page is the very first to have been rendered since
    /// the browser loaded the app. This will be reset on full reloads, and is
    /// used internally to determine whether or not we should look for
    /// stored HSR state.
    #[cfg(any(client, doc))]
    pub(crate) is_first: Cell<bool>,
    /// The app's *full* render configuration. Note that a subset of this
    /// is contained in the [`RenderMode`] on the engine-side for widget
    /// rendering.
    #[cfg(any(client, doc))]
    pub(crate) render_cfg: HashMap<String, String>,
    /// The app's templates and capsules for use in routing.
    #[cfg(any(client, doc))]
    pub(crate) entities: EntityMap<G>,
    /// The app's locales.
    #[cfg(any(client, doc))]
    pub(crate) locales: Locales,
    /// The browser-side translations manager.
    #[cfg(any(client, doc))]
    translations_manager: ClientTranslationsManager,
    /// The app's error views.
    #[cfg(any(client, doc))]
    pub(crate) error_views: Rc<ErrorViews<G>>,
    /// A reactive container for the current page-wide view. This will usually
    /// contain the contents of the current page, but it may also contain a
    /// page-wide error. This will be wrapped in a router.
    #[cfg(any(client, doc))]
    current_view: RcSignal<View<BrowserNodeType>>,
    /// A reactive container for any popup errors.
    #[cfg(any(client, doc))]
    popup_error_view: RcSignal<View<BrowserNodeType>>,
    /// The app's root div ID.
    #[cfg(any(client, doc))]
    root: String,

    // --- Engine-side only ---
    #[cfg(engine)]
    pub(crate) render_mode: RenderMode<G>,
    /// The currently active translator. On the browser-side, this is handled by
    /// the more fully-fledged `ClientTranslationsManager` type.
    ///
    /// This is provided to the engine-side reactor on instantiation. This can
    /// be `None` in certain error view renders.
    #[cfg(engine)]
    translator: Option<Translator>,
}

// This uses window variables set by the HTML shell, so it should never be used
// on the engine-side
#[cfg(any(client, doc))]
impl<G: Html, M: MutableStore, T: TranslationsManager> TryFrom<PerseusAppBase<G, M, T>>
    for Reactor<G>
{
    type Error = ClientError;

    fn try_from(app: PerseusAppBase<G, M, T>) -> Result<Self, Self::Error> {
        let locales = app.get_locales()?;
        let root = app.get_root()?;
        let plugins = &app.plugins;

        plugins
            .functional_actions
            .client_actions
            .start
            .run((), plugins.get_plugin_data())?;

        // We need to fetch some things from window variables
        let render_cfg =
            match WindowVariable::<HashMap<String, String>>::new_obj("__PERSEUS_RENDER_CFG") {
                WindowVariable::Some(render_cfg) => render_cfg,
                WindowVariable::None | WindowVariable::Malformed => {
                    return Err(ClientInvariantError::RenderCfg.into())
                }
            };
        // NOTE: This will be transmitted on all pages, including local redirection ones
        let global_state_ty = match WindowVariable::<Value>::new_obj("__PERSEUS_GLOBAL_STATE") {
            WindowVariable::Some(val) => {
                let state = TemplateState::from_value(val);
                if state.is_empty() {
                    // TODO Since we have it to hand, just make sure the global state creator really
                    // wasn't going to create anything (otherwise fail
                    // immediately)
                    GlobalStateType::None
                } else {
                    GlobalStateType::Server(state)
                }
            }
            WindowVariable::None => GlobalStateType::None,
            WindowVariable::Malformed => return Err(ClientInvariantError::GlobalState.into()),
        };

        Ok(Self {
            // This instantiates as if for the engine-side, but it will rapidly be changed
            router_state: RouterState::default(),
            state_store: PageStateStore::new(app.pss_max_size),
            global_state: GlobalState::new(global_state_ty),
            translations_manager: ClientTranslationsManager::new(&locales),
            // This will be filled out by a `.thaw()` call or HSR
            frozen_app: Rc::new(RefCell::new(None)),
            is_first: Cell::new(true),
            current_view: create_rc_signal(View::empty()),
            popup_error_view: create_rc_signal(View::empty()),
            entities: app.entities,
            locales,
            render_cfg,
            #[cfg(debug_assertions)]
            error_views: app.error_views.unwrap_or_default(),
            #[cfg(not(debug_assertions))]
            error_views: app
                .error_views
                .expect("you must provide your own error views in production"),
            root,
        })
    }
}

impl<G: Html> Reactor<G> {
    /// Adds `self` to the given Sycamore scope as context.
    ///
    /// # Panics
    /// This will panic if any other reactor is found in the context.
    pub(crate) fn add_self_to_cx(self, cx: Scope) {
        provide_context(cx, self);
    }
    /// Gets a [`Reactor`] out of the given Sycamore scope's context.
    ///
    /// You should never need to worry about this function panicking, since
    /// your code will only ever run if a reactor is present.
    pub fn from_cx(cx: Scope) -> &Self {
        use_context::<Self>(cx)
    }
    /// Gets the currently active translator.
    ///
    /// On the browser-side, this will return `None` under some error
    /// conditions, or before the initial load.
    ///
    /// On the engine-side, this will return `None` under certain error
    /// conditions.
    #[cfg(any(client, doc))]
    pub fn try_get_translator(&self) -> Option<Translator> {
        self.translations_manager.get_translator()
    }
    /// Gets the currently active translator.
    ///
    /// On the browser-side, this will return `None` under some error
    /// conditions, or before the initial load.
    ///
    /// On the engine-side, this will return `None` under certain error
    /// conditions.
    #[cfg(engine)]
    pub fn try_get_translator(&self) -> Option<Translator> {
        self.translator.clone()
    }
    /// Gets the currently active translator. Under some conditions, this will
    /// panic: `.try_get_translator()` is available as a non-panicking
    /// alternative.
    ///
    /// # Panics
    /// Panics if used before the initial load on the browser, when there isn't
    /// a translator yet, or if used on the engine-side when a translator is
    /// not available (which will be inside certain error views). Note that
    /// an engine-side panic would occur as the server is serving a request,
    /// which will lead to the request not being fulfilled.
    pub fn get_translator(&self) -> Translator {
        self.try_get_translator().expect("translator not available")
    }
    /// Switches the current locale to the given locale. This will navigate to
    /// the current page in the given locale.
    ///
    /// If a new page is being loaded, or if an error view is loaded, this will
    /// simply have no effect whatsoever (to avoid users trying to switch
    /// locales during a navigation and inadvertently causing a panic).
    ///
    /// # Panics
    ///
    /// This will panic if the given locale is not supported: use this only with
    /// hardcoded locale values! This will also panic if used in an error
    /// view without a translator.
    #[cfg(client)]
    pub fn switch_locale(&self, new_locale: &str) {
        let path = self.router_state.get_path();
        if let Some(path) = path {
            let curr_locale = self.get_translator().get_locale();
            let new_path = path.replace(&curr_locale, new_locale);
            sycamore_router::navigate(&new_path);
        }
    }
}

#[cfg(engine)]
impl<G: Html> Reactor<G> {
    /// Initializes a new [`Reactor`] on the engine-side.
    pub(crate) fn engine(
        global_state: TemplateState,
        mode: RenderMode<G>,
        translator: Option<&Translator>,
    ) -> Self {
        Self {
            state_store: PageStateStore::new(0), /* There will be no need for the state store on
                                                  * the
                                                  * server-side (but is still has to be
                                                  * accessible) */
            global_state: if !global_state.is_empty() {
                GlobalState::new(GlobalStateType::Server(global_state))
            } else {
                GlobalState::new(GlobalStateType::None)
            },
            render_mode: mode,
            translator: translator.cloned(),
        }
    }
}

/// The possible states a window variable injected by the server/export process
/// can be found in.
#[cfg(any(client, doc))]
pub(crate) enum WindowVariable<T: Serialize + DeserializeOwned> {
    /// It existed and coudl be deserialized into the correct type.
    Some(T),
    /// It was not present.
    None,
    /// It could not be deserialized into the correct type, or it was not
    /// instantiated as the correct serialized type (e.g. expected to find a
    /// string to be deserialized, found a boolean instead).
    Malformed,
}
#[cfg(any(client, doc))]
impl<T: Serialize + DeserializeOwned> WindowVariable<T> {
    /// Gets the window variable of the given name, attempting to fetch it as
    /// the given type. This will only work with window variables that have
    /// been serialized to strings from the given type `T`.
    fn new_obj(name: &str) -> Self {
        let val_opt = web_sys::window().unwrap().get(name);
        let js_obj = match val_opt {
            Some(js_obj) => js_obj,
            None => return Self::None,
        };
        // The object should only actually contain the string value that was injected
        let val_str = match js_obj.as_string() {
            Some(val_str) => val_str,
            None => return Self::Malformed,
        };
        let val_typed = match serde_json::from_str::<T>(&val_str) {
            Ok(typed) => typed,
            Err(_) => return Self::Malformed,
        };

        Self::Some(val_typed)
    }
}
#[cfg(any(client, doc))]
impl WindowVariable<bool> {
    /// Gets the window variable of the given name, attempting to fetch it as
    /// the given type. This will only work with boolean window variables.
    ///
    /// While it may seem that a boolean cannot be 'malformed', it most
    /// certainly can be if you think it is boolean, but it actually isn't!
    ///
    /// This is generally used internally for managing flags.
    pub(crate) fn new_bool(name: &str) -> Self {
        let val_opt = web_sys::window().unwrap().get(name);
        let js_bool = match val_opt {
            Some(js_bool) => js_bool,
            None => return Self::None,
        };
        // The object should only actually contain the boolean value that was injected
        match js_bool.as_bool() {
            Some(val) => Self::Some(val),
            None => Self::Malformed,
        }
    }
}
#[cfg(any(client, doc))]
impl WindowVariable<String> {
    /// Gets the window variable of the given name, attempting to fetch it as
    /// the given type. This will only work with `String` window variables.
    fn new_str(name: &str) -> Self {
        let val_opt = web_sys::window().unwrap().get(name);
        let js_str = match val_opt {
            Some(js_str) => js_str,
            None => return Self::None,
        };
        // The object should only actually contain the boolean value that was injected
        match js_str.as_string() {
            Some(val) => Self::Some(val),
            None => Self::Malformed,
        }
    }
}