Servers and exporting

There are two ways to run a Perseus app: you can serve it, or you can export it. To understand the difference, we'll need to dive a little deeper into how Perseus builds your app.

1. Preparation

In the first stage, Perseus compiles both itself and your project, and then takes a look at your PerseusApp. This tells it what to expect in terms of internationalization, templates, and capsules, and allows it to formulate a plan to move ahead with.

2. Building

Now, Perseus calls the build-time logic on every single template in your app, building as many as it possibly can. Those with capsules that would stall this process are rescheduled if that's permitted. Once this stage is done, Perseus compiles the render configuration, which defines all the templates in your app, and then terminates.

3. Wasm building

(White lie: this actually happens in parallel with steps 1 and 2.)

Next, Perseus will compile itself and your app to Wasm. Once this is done, there's no further processing performed on the Wasm bundle unless we're in release mode (in which case it's optimized to the nth degree).

4a. Serving

From here, if you've chosen to serve your app, Perseus will take the engine it used to build your app and repurpose that for serving your app (one binary does both, reducing compilation times substantially). This will spin up a server according to your #[perseus::main(..)] settings, and that will run until your terminate it, serving your app where you've specified.

This server responds to each request by passing it through special pathways that are capable of calling request-time logic, state amalgamation, revalidation, and all manner of other things. This part of Perseus involves just-in-time nested capsule resolution, which is by far the most complex part of Perseus. All this has to be done upon receiving a user's request, so this binary is deliberately optimized for speed (meaning it can easily blow out to very large sizes) in release mode.

Server integrations

Since Perseus tries to be as open as possible, it allows you to provide a custom function to #[perseus::main(..)] that will run your server. Usually, you'll just use the default server provided by one of the integrations, but you can also customize this however you like..

Server integrations are special crates, like perseus-axum, that provide the boilerplate to host Perseus through a particular server framework. Currently, Perseus has server integrations for Actix Web, Warp (although Warp itself appears to be unmaintained), and Axum. All of these have a dflt-server feature flag, which you can enable to gain access to the perseus-<integration-name>::dflt_server function, which will spin up a server that just hosts Perseus.

However, most apps also have several API routes associated with them, especially if you're working with a database. Since you can provide a custom function to host Perseus, you can also add arbitrary API routes. You can take a look at the custom server example for further details on this, or take a look at the source code for the server integration you're using.

Note: due to this bug, the Warp integration must currently be used with the warp-fix-171 crate, rather than the warp crate itself. As Warp itself appears to no longer be maintained, this situation is unlikely to change any time soon.

Writing a server integration

If you're unhappy with the defaults a particualr server integration provides, you are strongly encouraged to modify it yourself, which isn't a difficult process at all. Deliberately, Perseus abstracts nearly all serving functionality into the core, behind a type called Turbine, a static reference to which is provided to server integrations. This means all you're doing in a server integration is spinning up the right routes. In fact, all server integrations follow a pre-defined pattern of exactly what they have to do, making it much easier to write and modify them for your needs. If you really want, you can even fly solo without a server integration, and implement everything yourself (although this is not recommended unless you have very specific requirements).

Currently, Perseus has no support for arbitrary middleware, and modifying the server integration is the only way to do this. Bear in mind, however, that the Axum integration is only one file with 173 lines of code and comments in it --- these integrations are designed to be tweaked!

4b. Exporting

Alternately, if you've chosen to export, Perseus will mimic what the server does a little. It will first rearrange all the files the build process generated into folders that mimic the structure of the requests the client will send (e.g. things in dist/static get moved to dist/exported/.perseus/static), and then it will loop through all the pages and create initial load files for all of them. If you were serving your app, Perseus would insert page fragments into your index view at request-time, but exported apps do this ahead-of-time so no processing is required at request-time.

The output of exporting is a folder (dist/exported) that contains everything needed to run your app. Importantly, exported apps don't have access to any request-time features, like incremental generation or revalidation, and using any of these will cause the build process to neatly fail. However, exported apps are often a lot easier to deploy. For example, deploying this website is just a matter of uploading the generated static files to the gh-pages branch, and GitHub oes the rest for us (because there's no server involved, it's much easier to orchestrate these kinds of deployments). Exported apps are also usually cheaper to host.

When you run perseus deploy -e, Perseus will build your Wasm in release mode and move everything into a pkg/ folder that's fully optimized for production.

Note: perseus export -s exists to spin up a miniature file server to avooid your needing to bring your own in development.

Error pages in exported apps

One thing exported apps often struggle with is proper error handling. Once the Wasm bundle has been delivered to the client, they're fine and dandy, and can display all the errors they like, but the server-side is trickier. When Perseus controls it, it can carefully format error pages with exactly the right information, but typical file servers aren't quite so subtle. Especially for internationalized apps, this can be a problem. The best solution is to export your error pages to static files, which can be done like so:

perseus export-error-page --code 404 --output pkg/404.html

Here, we export the 404 page to pkg/404.html, where is will be picked up and served in the event of a 404 error by most file hosts. However, since we don't know the user's locale in advance, we can't localize this page appropriately, or even send the right translations. For apps not using i18n, this won't be a problem, but i18n-ed apps should prefer serving over exporting where possible.

Note that adding -s to a perseus export command in development will automatically export your 404 page, but the deployment system will not do this, so you may wish to make this a separate stage in your deployment process, depending on whether or not your file host supports this pattern.