One of the things we've run into as we've worked with customers and developed our own examples at
Cosmonic is the need to serve UIs that are consuming services you are running inside of wasmCloud.
Our own examples required you to either run the UI using npm
or to run a docker image. This felt
less than ideal and didn't fit with our vision of WebAssembly being the future of distributed
computing.
We just released a new version of the petclinic example that demonstrates how you can bundle up a UI for your application into a single actor. Now when you start the full petclinic example, the API and UI are served from the same place
How it works
This example works by including the assets in the compiled Wasm module. This does make the module larger than normal, but has the benefit of being completely portable. If you haven't seen the petclinic example before, there is an "API gateway" actor called Clinic API that dispatches requests to the appropriate actors. In this version, we dispatch any non-API calls to the UI actor to fetch the requested assets using an actor-to-actor call. We specifically chose to make the UI a separate actor to maintain separation of concerns and keep the code easy to understand.
The UI actor uses the rust-embed crate to include the bytes in
compiled module, accessible by path (as if they were on disk). Many languages have this support, so
as wasmCloud continues to support more languages, this same pattern should work in any language. We
use the build.rs
file to run the npm
command to generate static assets, and then we include the
assets like so:
#[derive(RustEmbed)]
#[folder = "./dist"]
struct Asset;
Yep, that's it! The actor then receives a path from the Clinic API actor and checks if that "file" exists and returns it if it does, along with an optional MIME type. The interface we use for the actor to actor call is very straightforward. If people find it useful for creating UIs, please let us know and we can look at making it a maintained interface. You can view the source code to see the full example for the UI actor.
An interface definition is a contract between actors and providers. It defines the set of operations that can be made from actor to provider. You can read more about them in the wasmCloud docs.
One other thing to note here is that this example takes advantage of the automatic streaming/chunking provided by wasmCloud. You can have some decently large files you'll need to send back (in our case, it was a CSS file). If they are large enough, the underlying lattice code will automatically chunk up the data when it sends it. This is entirely transparent to your actors and providers, which is part of the reason this is so easy. In fact, this is all that is required to send an asset back:
if let Some(file) = Asset::get(trimmed) {
return Ok(GetAssetResponse {
found: true,
asset: Vec::from(file.data),
content_type: mime_guess::from_path(trimmed)
.first()
.map(|m| m.to_string()),
});
}
Other possible patterns
We wanted to call out that this is not the only pattern for serving a UI. Another possible solution
here if you have large or common/shared UI assets is storing those assets in a blobstore. Your
equivalent of the petclinic UI actor could then use the wasmcloud:blobstore
contract (wasmCloud
has an S3 implementation
and an on disk
implementation in progress) to fetch the assets and return them rather than embedding them
We also have many ideas for the future (possibly including embedding assets alongside the actor in a bindle) and would love to highlight any other ways of doing this that the community comes up with!
What we learned
One of the great side effects of this work was discovering several bugs in the automatic chunking in
the wasmCloud host. Specifically, we found that a code path hadn't been exercised properly. Because
of this, a message body that should have been empty when it was encoded to send across the wire was
instead a nil
, which encodes to... well... nothing.
We also learned that it is entirely possible to build a full application (from UI all the way to backend services) in wasmCloud. That might seem painfully self-evident here at the end of this post, but it is important to note that this is why we dogfood (or "drink your own champagne" as we like to say) our own project. Many of the wasmCloud maintainers also work for Cosmonic, which is powered entirely by a wasmCloud backend, and we want to use and stress test wasmCloud to make it better for everyone.
Next Steps
Hopefully this is helpful for those of you who are building applications on wasmCloud! Please feel free to comment or share your examples of how you do this. Or, if you are new to wasmCloud, try running the example, checking out our getting started docs, or joining our Slack.