diff --git a/docs/next/en-US/SUMMARY.md b/docs/next/en-US/SUMMARY.md index aa1b1d00af..5750794452 100644 --- a/docs/next/en-US/SUMMARY.md +++ b/docs/next/en-US/SUMMARY.md @@ -24,7 +24,7 @@ - [Error views](/docs/fundamentals/error-views) - [Hydration](/docs/fundamentals/hydration) - [Static content](/docs/fundamentals/static-content) -- [Setting headers](/docs/fundamentals/headers) +- [Heads and headers](/docs/fundamentals/head-headers) - [Styling](/docs/fundamentals/styling) - [Working with JS](/docs/fundamentals/js-interop) - [Servers and exporting](/docs/fundamentals/serving-exporting) diff --git a/docs/next/en-US/fundamentals/head-headers.md b/docs/next/en-US/fundamentals/head-headers.md new file mode 100644 index 0000000000..1865402081 --- /dev/null +++ b/docs/next/en-US/fundamentals/head-headers.md @@ -0,0 +1,43 @@ +# Heads and headers + + So far, there are two critical parts of a webpage that we've largely ignored: the document ``, and the HTTP headers. The former is used to store static page metadata, such as the page title, while the latter can be used for all sorts of things, such as to control resource caching and manage cookies. + + Generally, in Perseus, you'll use the head much more than you'll use headers, simply because Perseus generally encourages an environment that is fairly siloed from the features of browsers themselves: for example, rather than setting a cookie through the `Set-Cookie` HTTP header, it's far more common in Perseus to just provide the value of the cookie through the state that's sent to the client, and then to set it there. What you choose to do is a matter of personal preference, but Perseus is generally built around state, not things like headers. + + *Note: for reading headers from the client to inform your state generation logic, see [request-time state generation](:state/request).* + +## Setting the head + +Heads work very much like views in Perseus: they're set on a template-by-template basis, and can take the state of the page, allowing them to specialize as necessary for pages within the templates on which they're set. + +Here's an example of setting the head without using any state: + +``` +{{#include ../../../../examples/core/basic/src/templates/about.rs}} +``` + +Note the use of `#[engine_only_fn]`, since Perseus will prerender the head of each page on the engine-side, as early as it can, and that will be transmitted as a static string to the client for further rendering. While this does mean you could theoretically do something like read from a file in your head function, this is not recommended, since the function is not `async`, and will block the rest of the build process or server, so you should prefer to do that sort of thing when generating state. + +Note also the use of `SsrNode` in the return type, which reflects that this will *always* be prerendered to a string on the engine-side. + +To access the state in the head, use `.head_with_state()` on `Template` instead of `.head()`, and have your function accept a second argument for your (unreactive) state type. + +If you want to return an error from your head function for some reason, you can, and that will lead to the entire page failing. Generally, this is not desired. This behaves similarly to the state generation functions, which you can read more about [here](:state/build). + +For information about setting a general *index view*, see [here](:fundamentals/perseus-app). + +## Setting headers + +When you need to set headers, you can do so with a function of the same form as the one you use to set page heads: it should be synchronous and take two arguments if you're accessing your state, or one if you're not. + +Here's a more fully-fledged example that sets the custom `X-Greeting` header with the contents of some generated state: + +``` +{{#include ../../../examples/core/set_headers/src/templates/index.rs}} +``` + +Note the use of `.set_headers_with_state()` on `Template`, but this could also be `.set_headers()` if you didn't need access to your state type. + +What is by far most important about this function is its return type, which comes from the [`http`](https://docs.rs/http/latest/http) crate, conveniently re-exported from Perseus on the engine-side. You'll need a return a [`HeaderMap`](=http/header/struct.HeaderMap@perseus), specifically, into which you can insert individiual headers, similarly to a `HashMap`. + +Just like the head function, this can also return an error if you'd like it to, or it can be infallible, as it is here. diff --git a/docs/next/en-US/state/amalgamation.md b/docs/next/en-US/state/amalgamation.md new file mode 100644 index 0000000000..c78ec45c2e --- /dev/null +++ b/docs/next/en-US/state/amalgamation.md @@ -0,0 +1,13 @@ +# State amalgamation + +There are quite a few cases when you're using the state generation platform where you might like to generate state at both build-time *and* request-time, and Perseus has several ways of handling this. Generally, the request-time state will just completely override the build-time state, which is a little pointless, since it doesn't have access to the build-time state, and therefore there would really be no point in even using build-time state. However, you can also specify a custom strategy for resolving the two states, which is called *state amalgamation*. To our knowledge, Perseus is currently the only framework in the world that supports this (for some reason, since it's really not that hard to implement). + +Like [other state generation functions](:state/build), your state amalgamation function can be either fallible (with a [`BlamedError`](=prelude/struct.BlamedError@perseus)) or infallible, and it has access to a [`StateGeneratorInfo`](=prelude/struct.StateGeneratorInfo@perseus) instance. It's also asynchronous, and returns your state. The difference between it and other functions is that it also takes, as arguments, your build-time and request-time states (it does *not* take the HTTP request, so you'll have to extract any data from this that you want and put it into your request-time state). Here's an example of using it (albeit a rather contrived one): + +``` +{{#include ../../../examples/core/state_generation/src/templates/amalgamation.rs}} +``` + +Real-world examples of using state amalgamation are difficult to find, because no other framework supports this feature, although there have been requests for it to be supported in some very niche cases in the past. Since it involves very little code from Perseus, it is provided for those niche cases, and for cases where it would be generally useful as an alternative solution to a problem. + +One particular case that can be useful is having an `enum` state with variants for build-time, request-time, and post-amalgamation. The build-time state can be used for anything that can be done that early, and then the request-time state performs authentication, while the amalgamation draws it all together, ensuring that only the necessary stuff is actually sent to the client. Unfortunately, doing this would require a manual implementation of the traits that `ReactiveState` would normally implement, since it doesn't yet support `enum`s (but it will in a future version). diff --git a/docs/next/en-US/state/freezing-thawing.md b/docs/next/en-US/state/freezing-thawing.md new file mode 100644 index 0000000000..973a13f88a --- /dev/null +++ b/docs/next/en-US/state/freezing-thawing.md @@ -0,0 +1,35 @@ +# Freezing and thawing + +One of the most unique, and most powerful features of the Perseus state platform is its system of *state freezing*. Imagine this: all your reactive (and unreactive) state types implement `Serialize` and `Deserialize`, right? We also have an internal cache of them that monitors all the updates that occur to the states of the last *N* pages a user has visited (by default, *N* is 25). So what if we iterated through all of those, serialized them to a string, and stored that? It would be a fullly stringified representation of the state of the app. And, if you build your app with all reactive components built into your state type (i.e. not using rogue `Signal`s that aren't a part of your page state), then you could restore your entire app perfectly from this string. + +Since v0.3.5, that has been built into Perseus. + +In fact, it's this feature that powers one of Perseus' most powerful development features: *hot state reloading* (HSR). In JS-land, there's *hot module reloading*, where the bundlers intelligently only swaps out the tiny little chunks of JS needed to update your app, allowing you, the developer, to stay in the same place while you're developing. If you're four states deep into debugging a login form, not having to be thrown back to the beginning every time you reposition a button is something you will *really* appreciate! However, this seems impossible in Wasm, because we don't have chunking yet. Perseus changes this by implementing state freezing/thawing at the framework level, allowing Perseus to automatically freeze your entire app's state, save it into the browser, reload the page to get the new code, and then instantly thaw your app, meaning the only times you will get thrown back to the beginning of that login form are when you change your app's data model. + +## Understanding state freezing + +State freezing can be slightly difficult to understand at an implementation level, because of the complexity of the internals of Perseus. Generally though, you can think of it like this: all your pages are literally having their states serialized to `String`s, and then those are all being combined with your global state (if you have one), and some other details, like the current route. This can then all be used by Perseus to *thaw* that string by deserializing everything and reconstituting it. + +## The process of thawing + +Critically, Perseus **does not** restore your state all at once, and this can be difficult to wrap your head around. The problem is that Perseus doesn't record any of your state types internally: it gets them from your view functions, and that means it can't thaw all your state at once, because it doesn't know what to deserialize your states into. For all it knows, your page states might by `u8`s! So, Perseus stores all the frozen state internally, and, each time the user goes to a new page, it checks if there's some frozen state known for that page, deserializing it if it can. If this fails, a popup error will be emitted, which can usually be solved by reloading the page to dispose of the corrupted frozen state. (Note that most accidental corruptions would break the very JSON structure of the thing, and would be caught immediately.) This also goes for the global state (frozen state is checked on the first `.get_global_state()` call to [`Reactor`](=prelude/struct.Reactor@perseus)). + +Note that Perseus will also automatically navigate back to the route the user was on when their state was thawed. + +You can control many aspects of thawing, including whether frozen state or new state is preferred, on a page-by-page basis using the [`ThawPrefs`](=state/struct.ThawPrefs@perseus), which you can read about at that link. + +## Example + +Here's a more complex example of using state freezing. There are two inputs, one for the global state, and one for the page state, which will be used to reactively set them, and then a button that freezes the whole app (using the `reactor.freeze()` method, which really is all you need to do!). For demonstration purposes, that's then synchronized to an input that takes in state that can be used to thaw the app, which is a slightly more complex (and fallible) process. Note the use of `#[cfg(client)]`, since state freezing/thawing can only take place on the client-side. + +``` +{{#include ../../../examples/core/freezing_and_thawing/src/templates/index.rs}} +``` + +## Storing frozen state + +Freezing your app's state can be extremely powerful, and it's often very useful to simply store this frozen state in a database, allowing your users to return to exactly where they left off after they log back in, or something similar. However, there is also the option of storing the state in the browser itself through [IndexedDB], a database that can be used to store complex objects. Interfacing with IndexedDB is extremely complex in JS, let alone in Wasm (where we have to use `web-sys` bindings), so Perseus uses [`rexie`](https://docs.rs/rexie/latest/rexie) to provide a convenient wrapper when the `idb-freezing` feature flag is enabled. This is managed through the [`IdbFrozenStateStore`](=state/struct.IdbFrozenStateStore@perseus) type, which uses a named database. If you like, you can do this manually: this type is provided as a common convenience, and because it's used internally for HSR. + +## Offline state replication + +*Coming soon!* diff --git a/examples/core/state_generation/src/templates/amalgamation.rs b/examples/core/state_generation/src/templates/amalgamation.rs index c7b373511d..404bd2dc07 100644 --- a/examples/core/state_generation/src/templates/amalgamation.rs +++ b/examples/core/state_generation/src/templates/amalgamation.rs @@ -19,7 +19,8 @@ pub fn get_template() -> Template { // We'll generate some state at build time and some more at request time .build_state_fn(get_build_state) .request_state_fn(get_request_state) - // But Perseus doesn't know which one to use, so we provide a function to unify them + // But Perseus would usually just override the build state with request + // state, so we provide a custom function to unify them .amalgamate_states_fn(amalgamate_states) .view_with_state(amalgamation_page) .build() diff --git a/packages/perseus/src/state/freeze.rs b/packages/perseus/src/state/freeze.rs index 1d41fdbeab..eea90ce641 100644 --- a/packages/perseus/src/state/freeze.rs +++ b/packages/perseus/src/state/freeze.rs @@ -39,6 +39,9 @@ pub struct ThawPrefs { /// but if thawing occurs later in an app, it may be desirable to override /// active state in favor of frozen state. These preferences allow setting an /// inclusion or exclusion list. +/// +/// In apps using internationalization, locales should not be provided here, +/// they will be inferred. #[derive(Debug, Clone)] pub enum PageThawPrefs { /// Include the attached pages by their URLs (with no leading `/`). Pages @@ -57,7 +60,7 @@ pub enum PageThawPrefs { impl PageThawPrefs { /// Checks whether or not the given URL should prioritize frozen state over /// active state. - pub(crate) fn should_prefer_frozen_state(&self, url: &str) -> bool { + pub(crate) fn should_prefer_frozen_state(&self, url: &PathWithoutLocale) -> bool { match &self { // If we're only including some pages, this page should be on the include list Self::Include(pages) => pages.iter().any(|v| v == url),