Learn how to use Crux in your project.
Follow the readme in the project's repository on Github.
Read the API documentation
Watch the introductory talk at the recent Rust Nation 2023 conference in London.
You can also join the friendly conversation on our Zulip channel.
Note, that Crux is experimental and currently under active development (probably not ready for use in production apps just yet). However, the master branch should always be working well, and we will try to keep the examples and documentation up to date as we go. We do think that the API has now settled, so have a play! :-)
The fundamental architectural concept is the strict separation of pure computational tasks from tasks that cause side effects. This is similar to the way Elm works.
In the above diagram, the inner "Core" is compiled and linked to the outer "Shell" on each platform as a library:
In fact, because WebAssembly (Wasm) is one of the compilation targets, the core must remain side-effect free, due to the sandboxed nature of the Wasm runtime environment.
As such, the core is completely isolated and secure against software supply-chain attacks, as it has no access to any external APIs. All it can do is perform pure calculations and keep internal state.
Following the Elm architecture, the core defines the key component types within the application:
Event
— an enum
describing the events which the core can handleModel
— describes the internal state of the applicationViewModel
— represents information that should be displayed to the userThe former two are tied together by the update
function, familiar from Elm,
Redux or other event sourcing architectures, which currently has this type
signature:
fn update(
&self,
event: Event,
model: &mut Model,
capabilities: &Capabilities,
)
The job of the update
function is to process an Event
, update the model
accordingly, and potentially request some side-effects using capabilities.
The enclosing platform native "Shell" is written using the language appropriate for the platform, and acts as the runtime environment within which all the non-pure tasks are performed. From the perspective of the core, the shell is the platform on which the core runs.
Following the Elm architecture, the interface with the core is message based. This means that the core is unable to perform anything other than pure calculations. To perform any task that creates a side-effect (such as an HTTP call or random number generation), the core must request it from the shell.
The core has a concept of Capabilities — reusable interfaces for common side-effects — supporting fire-and-forget, request/response, and streaming semantics.
The only built-in capability is Render
. But this repository contains a few
capabilities at various stages of maturity, and you can easily write your own if
you want to:
Render
(ask UI to render the ViewModel) —
source, built-in to crux_core
,
request onlyHttp
(full HTTP implementation based on the
Surf API) — source,
crate, request/responseKeyValue
(basic key-value store API) — source,
crate, request/responseTime
(get current time, notify after duration, notify at instant) —
source, crate,
request/responsePlatform
(get the current platform) — source,
crate, request/responseSSE
(basic Server-Sent Events) —
source,
request/streamingPubSub
(pub sub with streaming) —
source,
request/response/streamingTimer
(timer start, finish, cancel) —
source,
request/response/streamingDelay
— part of
tutorial
in the bookThis means the core interface is simple:
process_event: Event -> Vec<Request>
- processes a user interaction event
and potentially responds with capability requests. This is the API for the
driving side in the above diagram.handle_response: (uuid, SomeResponse) -> Vec<Request>
- handles the response
from the capability and potentially follows up with further requests. This is
the API for the driven side in the above diagram.view: () -> ViewModel
- provides the shell with the current data for
displaying user interfaceUpdating the user interface is considered a side-effect and is provided by the
built-in Render
capability.
This design means the core can be tested very easily, without any mocking and stubbing, by simply checking the Input/Output behaviour of the three functions.
The Foreign Function Interface allowing the shell to call the above functions is provided by Mozilla's UniFFI on a mobile device, or in the browser, by wasm-pack.
In order to both send more complex data than UniFFI currently supports, and enforce the message passing semantics, all messages are serialized, sent across the boundary, then deserialized using serde-generate which also provides type generation for the foreign (non-Rust) languages.
This means that changes to types in the core, especially the Event
and
Request
types, propagate out into the shell implementations and cause type
errors where appropriate (such as an exhaustive match on an enum check).
Three types of message are exchanged between the application and the core.
Event
are sent from the Shell to the Core in response to an
event happening in the user interface (the driving side). They start a
potential sequence of further message exchanges between the shell and the
core. Messages are passed on unchanged.Request
are sent from the Core to the Shell to request the
execution of some side-effect-inducing task. The Core responds with zero or
more Request
messages after receiving an Event
message (the driven
side).Request
messages contain the inputs for the requested side-effect, along with
a uuid
used by the core to pair requests and their responses together. The
exact mechanics are not important, but it is important for the request's uuid
to be passed on to the corresponding response.
A typical message exchange cycle may look like this:
Event
process_event
function passing the Event
as an
argumentRequest
messages to the ShellIn the simplest case, the Core will respond to an Event
by returning the
single Request
- render.
This requests that the Shell re-renders the user interface. When Render
is the
only response from the Core, the message cycle has completed and the Core has
now "settled".
In more complex cases however, the Core may well return multiple Request
s;
each of which instructs the Shell to perform a side-effect-inducing task such
as:
Many of these side-effecting-inducing tasks are asynchronous. The Shell is
responsible for passing responses back to the core (to the handle_response
function), which may respond with further requests.
This exchange continues until the core stops requesting further side-effects
(typically the last side-effect requested would be Render
).
Refer to examples/counter README
Refer to the Getting Started section of the tutorials.
Crux is kindly sponsored by the following organizations. Your help is very much appreciated.
Red Badger is the digital product consultancy trusted by blue chips and global brands. Our product design and technical pedigree allow us to craft high-impact digital products customers want. We use modern engineering approaches to deliver sustainable change. And embed digital capabilities to power continuous innovation.
Zulip is an open-source modern team chat app designed to keep both live and asynchronous conversations organized.
Zulip sponsor Crux by providing our Zulip server — thank you Zulip!