Rewriting Comlink in Rust (kinda)
Tags: rust, webassembly
Published: 2023-08-26 15:37:56


In this post I share my thought process for creating a Rust wrapper around `postMessage` and making it friendlier with a procedural macro. I then use it to build a tally counter that runs on a web worker. [The resulting proof of concept](https://github.com/voxelstack/atlas) is available on my GitHub.

> Much of what I describe here was done for fun and learning, but there is a practical takeaway for WebAssembly + Svelte: observables serve as Svelte stores. That means if your WebAssembly code can create an observable and return it to JavaScript, `{$observable}` will be reactive even for changes on the WebAssembly side. Skip to "Finishing touches" for more details on that. I found out about observables being usable as stores after reading an [article about RxJs with Svelte](https://timdeschryver.dev/blog/unlocking-reactivity-with-svelte-and-rxjs) by Tim Deschryver.

## The spark
WebAssembly already looked very promising as a way to satisfy my cravings for lower-level 3D graphics, but there are many new and exciting things happening: [wasm-bindgen](https://github.com/rustwasm/wasm-bindgen) means I can write Rust, which I'm a big fan of; [wgpu](https://wgpu.rs/) lets me move on from OpenGL and go back to studying modern graphics APIs; [wasm-bindgen-rayon](https://github.com/GoogleChromeLabs/wasm-bindgen-rayon) gives me threads; and finally [OffscrenCanvas](https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas) allows me to do all the rendering inside a web worker, making all of this look pretty similar to how you'd write a native renderer.

With [SvelteKit](https://kit.svelte.dev/) as an application framework, we gain access to [mdsvex](https://mdsvex.com/): a way to write interactive markdown.

## The destination
![A diagram with a dotted vertical line down the middle. On the top left is a monitor icon labeled "surface", and on the bottom is a blue wireframe cube labeled "client". From the client there is an arrow pointing to the surface. On the top right is a group of blue gears labeled "worker pool". On the bottom right is another blue wireframe cube labeled "server"; from the server, arrows point to several of the gears. A bidirectional arrow connects the client and server, crossing the dotted vertical line. Where this arrow intersects the vertical line, there is an envelope.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d5kpewqyicr1xumrlj4a.png)

The original idea was to have a web worker drawing to a canvas and a DOM-based UI that controls it. In the image above, everything to the left of the horizontal line exists within the main thread, while everything to the right is on web workers.

It all runs on the browser, but to make it easier to reason about, I call the primary web worker a "render server" while the code running on the main thread is the "client". Each gear represents a web worker spawned by wasm-bindgen-rayon, exposed to Rust as threads. The envelope represents data crossing the `postMessage` boundary.

## The journey
[Comlink](https://github.com/GoogleChromeLabs/comlink) simplifies communication with web workers by providing an RPC implementation built on top of `postMessage`. I decided to write the client and server in Rust without using Comlink. A way to communicate with the render server was the first problem to solve, and for that I needed a Rust-friendly way to send messages between the main thread and a web worker.

### Build setup
The client and server code are in separate crates, and each gets built into its own WebAssembly package. This build setup increases the final bundle size, doubling the overhead of both wasm-bindgen and the communication code. Building a single package and being careful to only use client code on the main thread and server code on a worker addresses that. However, wasm-bindgen-rayon generates code that should only run on workers, so to avoid having to work around that I opted to keep them separate on this demo.

> A small takeaway from this setup is that it is possible to organize code with a cargo workspace while using wasm-pack: [the build command can take a path](https://github.com/voxelstack/atlas/blob/f6b16ab1b759cb7a2a74154198ba12089cfbbb7d/package.json#L8-L9). While this means wasm-pack builds the individual crates and doesn't see the workspace, the language server does, so it's still helpful. Just remember that the members will not [inherit keys set on the workspace](https://doc.rust-lang.org/cargo/reference/workspaces.html?highlight=workspace#the-package-table).

### Something that can communicate
[Many things can send messages](https://developer.mozilla.org/en-US/search?q=postMessage) and [many things implement `EventTarget`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget). Our use case is exchanging messages between the main thread and a web worker. That involves working with two interfaces: [`Worker`](https://developer.mozilla.org/en-US/docs/Web/API/Worker) will be used on the client to send messages and listen to incoming responses from the server, and [`DedicatedWorkerGlobalScope`](https://developer.mozilla.org/en-US/docs/Web/API/DedicatedWorkerGlobalScope) will be used on the server to send messages and listen to incoming responses from the client.

Messages are a common part of these two interfaces: they both have a `postMessage`, and both implement `EventTarget` (specifically, they can receive "message" events). The first step was to define [a trait for things that can send and receive messages](https://github.com/voxelstack/atlas/blob/f6b16ab1b759cb7a2a74154198ba12089cfbbb7d/src/atlas/comms/src/port.rs#L6-L12). It describes the ability to send messages, [transfer objects](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Transferable_objects), and add or remove event listeners:

```rust
pub trait RawPort {
    fn send_raw(&self, message: JsValue);
    fn transfer_raw(&self, message: JsValue, transfer: JsValue);
    fn add_raw_listener(&self, listener: &js_sys::Function);
    fn remove_raw_listener(&self, listener: &js_sys::Function);
    fn start(&self) {}
}
```
> The `start` method is needed when [working with message ports](https://developer.mozilla.org/en-US/docs/Web/API/MessagePort/start).

`raw` indicates that these methods work with `JsValue`s (the Rust representation of an object owned by JavaScript). We'll be using the implementations of `RawPort` for [`Worker`](https://github.com/voxelstack/atlas/blob/f6b16ab1b759cb7a2a74154198ba12089cfbbb7d/src/atlas/comms/src/port.rs#L14-L34) and [`DedicatedWorkerGlobalScope`](https://github.com/voxelstack/atlas/blob/f6b16ab1b759cb7a2a74154198ba12089cfbbb7d/src/atlas/comms/src/port.rs#L36-L56).

Converting messages to `JsValue`s and back shouldn't be done manually, but with `RawPort` it is possible to create an entity that sends Rust types:

```rust
struct Port(Box<dyn RawPort>);
impl Port {
    pub fn send<M>(&self, message: M) {
        todo!();
    }

    pub fn add_listener(&self, listener: Closure<dyn Fn(MessageEvent)>) -> Listener {
        todo!();
    }
}
```

The `Listener` handle is helpful for any situation where you want to pass a Rust closure to JavaScript. Since JavaScript has no concept of ownership, the `listener` closure would be dropped when `add_listener` returns, and the event handler would access an invalid reference. [The example on the wasm-bindgen book](https://rustwasm.github.io/docs/wasm-bindgen/examples/closures.html#web-sys-closures) "solves" this by calling `closure.forget()`. That leaks memory, so it's worth taking a brief detour to talk about it.

#### Passing Rust closures to JavaScript
Since `RawPort` calls a listener closure whenever it receives an event, active listener closures must be valid for as long as the event listener is attached. We can achieve that by storing the closure on a container whose lifetime doesn't exceed the lifetime of the port.

```rust
pub struct Listener<'a> {
    owner: & 'a dyn RawPort,
    inner: Closure<dyn Fn(MessageEvent)>,
}
```
> Here we need a reference to the owner to clear the event listener. If we didn't, `_marker: PhantomData<& 'a ()>` would be enough to tie the lifetime.

Whoever holds this handle should be able to clear the associated event listener (in case they are no longer interested in that event), but more importantly, the event listener must be cleared if the closure is dropped:
```rust
impl<'a> Listener<'a> {
    pub fn clear(self) {
        self.remove_listener();
    }

    fn remove_listener(&self) {
        self
            .owner
            .remove_raw_listener(self.inner.as_ref().unchecked_ref());
    }
}

impl<'a> Drop for Listener<'a> {
    fn drop(&mut self) {
        self.remove_listener();
    }
}
```
> The public method `clear` consumes `self` because this handle should not be reused after being cleared.

Now `add_listener` can return a `Listener` handle, and the event handler will never call a closure that has been dropped.
```rust
impl Port {
    pub fn add_listener(&self, listener: Closure<dyn Fn(MessageEvent)>) -> Listener {
        self.0.add_raw_listener(listener.as_ref().unchecked_ref());

        Listener {
            owner: self.0.as_ref(),
            inner: listener,
        }
    }
}
```

### What are we sending?
Implementing the `send` method is harder. On the first code snippet, the message type was `M`. That is convenient because it allows sending and receiving whatever type works best for the situation. Imagine something like this:

|Request (client)|Response (server)|
|:-|:-|
|`Set { key: "seed", value: "iLikeRocks"}`|`Ok(())`|
|`Set { key: "skyColor", value: "blue"}`|`Err("skyColor expects a hex string")`|
|`Query { key: "memoryUsage" }`|`Ok(128)`|

I left out something important though. Behind the scenes, `send` uses `postMessage` and that matters:
* messages must be capable of crossing the Rust/JavaScript boundary;
* the data must keep its meaning after being serialized with [the structured clone algorithm](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm);
* some messages might require [transfering objects](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Transferable_objects) to the destination side (for example, when sending an `OffscreenCanvas`)

Since `post_message` sends `JsValue`s and those can cross the Rust/JavaScript boundary, it must be possible to convert messages to `JsValue`. An optional list of objects to transfer can be described as an `Option<JsValue>`. With those two requirements in mind, we can define a message as a type that can be converted to a tuple `(JsValue, Option<JsValue>)`: the content of the message and an an optional list of objects to transfer.
There are three content types to consider here, and I have named them as follows:
* `"raw"`: wasm-bindgen provides `Into<JsValue>`, `From<JsValue>` implementations for these types;
* `"serde"`: [arbitrary types that must be serialized](https://rustwasm.github.io/wasm-bindgen/reference/arbitrary-data-with-serde.html#serializing-and-deserializing-arbitrary-data-into-and-from-jsvalue-with-serde), we let `serde` handle those;
* `"shareable"`: nested messages composed by content types that fall under one of these three categories.

Finally, the receiving side must be able to convert a `JsValue` back into the original type.

We can write that as a trait. Read the following code as: "`Shareable` is something that can be sent using `postMessage` while possibly transferring some objects, and then read back as a Rust type on the receiving side".
```rust
pub trait Shareable:
    TryInto<(JsValue, Option<JsValue>), Error = ShareableError>
    + TryFrom<JsValue, Error = ShareableError>
{
}
```

Now, we can finally reason about what the implementation of our send method might look like. First we introduce a trait bound on `M`, requiring it to be `Shareable`. That allows converting messages into a `(data, transfer)` tuple. If there is nothing to transfer, send the data using `send_raw`. Otherwise, send the data and transfer some objects with `transfer_raw`.

```rust
pub fn send<M>(&self, message: M)
where
    M: Shareable,
{
    let (data, transfer) = message.try_into().unwrap();
    match transfer {
        Some(transfer) => self.transfer_raw(data, transfer),
        None => self.send_raw(data),
    }
}
```
> There should be error handling here (for example, if serialization fails).

That is enough. Any type `M` that implements `Shareable` can be sent between the main thread and a web worker using a `Port`. It is transparent too: Rust on both sides, no `JsValue`s involved.
```rust
pub struct M {
    // --snip--
}

impl Into<(JsValue, Option<JsValue>)> for M {
    // --snip--
}
impl TryFrom<JsValue> for M {
    // --snip--
}
impl Shareable for ClientMessage {}
```

```rust
// client.rs
let m = M::new();
port.send(m);
```

```rust
// server.rs
port.add_listener(Closure::new(|event: MessageEvent| {
    let m: M = event.data().try_into().unwrap();
    trace!(m);
}));
```
Going back in the git history, you can see that [this is what I used for my first tests](https://github.com/voxelstack/atlas/blob/fff1b302016e68ec27b15e84c74bde6664fc6eb0/src/atlas/comms/src/client.rs). Notice the TODO: `// TODO #[derive(Shareable)]`.

### `#[derive(Shareable)]`
This only looks convenient because I am hiding the implementations for `TryInto<(JsValue, Option<JsValue>)>` and `TryFrom<JsValue>`. 

[TryInto](https://github.com/voxelstack/atlas/blob/fff1b302016e68ec27b15e84c74bde6664fc6eb0/src/atlas/comms/src/client.rs#L13-L32) must be called before sending and does the following:
1. make a `JsArray` and push a string that identifies the type;
2. if the type is an enum, push a string that identifies the variant;
3. if there are any fields, convert their values to `JsValue` and push them to the array;
4. if there are things to transfer, make another JsArray and push the transferrable objects.

[TryFrom](https://github.com/voxelstack/atlas/blob/fff1b302016e68ec27b15e84c74bde6664fc6eb0/src/atlas/comms/src/client.rs#L38-L53) is called after receiving and does the following:
1. read the type identifier from the array;
2. if the type is an enum, read the variant identifier;
3. if there are any fields, read the `JsValue`s and convert them back to Rust types;
4. reconstruct the original message.

> An array representing `ClientMessage::Set("key", "value")` looks like this: `["ClientMessage", "Set", "key", "value"]`

These steps are straightforward, but implementing them for every transferrable type is repetitive, error-prone, and inconvenient. Thankfully, that is a perfect use case for [derive macros](https://doc.rust-lang.org/reference/procedural-macros.html#derive-macros).

Adding the derive attribute to a struct, enum, or union allows you to read the token stream that defines that item and create a new token stream that the compiler then appends to the source. [cargo-expand lets you see this in action](https://github.com/dtolnay/cargo-expand#example).

#### So long, and thanks for all the macros
After adding the macro, we are actually done and can derive the implementation for `Shareable`.

All you need to do to turn a Rust type into a valid message type is annotate it with `#[derive(Shareable)]` and possibly set some attributes on each item. It is possible to set the representation to be `raw`, `serde`, or `shareable`, which controls how to transform the value into a `JsValue`, and it is possible to mark items to transfer. Here is a practical example:
```rust
#[derive(Debug, Shareable)]
struct FontOptions {
    #[shareable(repr = "serde")]
    family: String,
    #[shareable(repr = "serde")]
    size: u8,
}

#[derive(Debug, Shareable)]
enum Message {
    Ping,
    RenderDebugString(
        #[shareable(repr = "serde")] String,
        #[shareable(repr = "shareable")] FontOptions
    ),
    CreateWindow {
        #[shareable(repr = "serde")]
        title: String,
        #[shareable(repr = "raw", transfer)]
        surface: OffscreenCanvas,
    },
}
```

> Check [the macro tests](https://github.com/voxelstack/atlas/blob/f6b16ab1b759cb7a2a74154198ba12089cfbbb7d/src/atlas/comms/src/lib.rs#L45-L582) to see incremental examples of the macro in action.

It derives `Shareable` for both enums and structs, handles arbitrary data with serde, works with nested shareable types, and possibly transfers values when posting messages. The macro generates all the required trait implementations, and the `Port` provides all the glue code to wrap types capable of sending and receiving messages.

> Rust macros are too broad of a topic to cover in detail here, so from [the entire macro derivation](https://github.com/voxelstack/atlas/blob/main/src/atlas/comms_derive/src/lib.rs) I chose a single highlight for this section: error handling.

> The [procedural macro workshop](https://github.com/dtolnay/proc-macro-workshop) was the best resource I could find on procedural macros. Jon Gjengset has [walkthroughs for some of the workshop projects](https://youtu.be/geovSK3wMB8).

#### Error handling
![A screenshot of a Tweet by mcc @mcclure111. The Tweet was posted at 4:30PM - 1 Jun 2018 and has 292 retweets, 962 likes and 20 replies. It reads: In C++ we don't say "Missing asterisk" we say "error C2664: 'void std::vector<block,std::allocator<_Ty>>::push_back(const block &)': cannot convert argument 1 from 'std::_Vector_iterator<std::_Vector_val<std::_Simple_types<block>>>' to 'block &&'" and I think that's beautiful.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0062ih9z15ntsdov70ek.png)

Compilation errors caused by generated code are notoriously annoying to debug and often result in unhelpful error messages. Let's make a typo while defining a repr:
```rust
#[derive(Debug, Shareable)]
pub struct WindowProxy {
    #[shareable(repr = "rraw", transfer)]
    handle: OffscreenCanvas
}
```

The macro derivation being done in Rust already does a lot to address this issue. My code for parsing the repr attribute looks similar to this:
```rust
match lit.to_string().as_ref() {
    "\"raw\"" => Repr::Raw,
    "\"serde\"" => Repr::Serde,
    "\"shareable\"" => Repr::Shareable,
    _ => todo!("invalid repr"),
}
```
Since the match must be exaustive, every typo will get caught by the catch-all arm, which is where we should handle this error. When dealing with conditional compilation or generated code, the [`compile_error!`](https://doc.rust-lang.org/std/macro.compile_error.html) macro can signal the compiler to fail. Just as an irrecoverable error during runtime should invoke `panic!`, an irrecoverable error during compilation time should invoke `compile_error!`.

Replacing the `todo!` with code that adds a `compiler_error!` invocation to the token stream is a good start: no wall of text and a helpful message. Still, the error only tells us something went wrong during the macro derivation.
```text
error: invalid repr
  --> lib.rs:28:17
   |
28 | #[derive(Debug, Shareable)]
   |                 ^^^^^^^^^
   |
   = note: this error originates in the derive macro `Shareable` (in Nightly builds, run with -Z macro-backtrace for more info)
```

The [`syn` crate](https://docs.rs/syn/latest/syn/) gives us a better way to represent errors using `syn::Error`: a type that can hold a `Span` along with a message. For our purposes, a `Span` is something that holds a start and an end index into a string, identifying a range of characters contained in the source code. For example, on the following snippet the `Span` could be used to determine where the markers (`^`) should start and where they should end.
```text
28 | #[derive(Debug, Shareable)]
   |                 ^^^^^^^^^
```

When parsing a literal into a repr, we have access to the span of that literal, and that allows us to create an error that along with the message also stores where in the source code our typo happened. Here is that snippet again, now much closer to the [actual implementation](https://github.com/voxelstack/atlas/blob/f6b16ab1b759cb7a2a74154198ba12089cfbbb7d/src/atlas/comms_derive/src/attrs.rs#L128-L133):
```rust
match lit.to_string().as_ref() {
    "\"raw\"" => Ok(Repr::Raw),
    "\"serde\"" => Ok(Repr::Serde),
    "\"shareable\"" => Ok(Repr::Shareable),
     _ => Err(syn::Error::new(lit.span(), "invalid repr, expected literal: \"raw\", \"serde\", or \"shareable\"")),
}
```

Now, the typo will cause a compiler error specifying what went wrong, which token that caused it, and how to fix it:
```text
error: invalid repr, expected literal: "raw", "serde", or "shareable"
  --> src\lib.rs:24:24
   |
24 |     #[shareable(repr = "rraw", transfer)]
   |                        ^^^^^^

error: could not compile `example` (lib) due to previous error
```

##### Handling macro derivation errors with `Result`
To render a `syn::Error` into a token stream that invokes `compiler_error!` we use `syn::Error::into_compile_error`. [Here is how that happens](https://github.com/dtolnay/syn/blob/b387b13f48e76468a3fc58e55559d2f41fa28a71/src/error.rs#L275-L322), note how it gets the `start` and `end` from the `Span` and uses those when constructing the token stream.

> Little fun fact: `syn` uses `Delimiter::Brace` when constructing the group, so this generates `::core::compile_error!{$message}`. Delimiters don't matter for `macro_rules!` macros: for example, `vec!(1, 2, 3)`, `vec! [1, 2, 3]`, and `vec!{1, 2, 3}` are all valid.

You can delegate your macro derivation to a function that returns a `Result`, and then consume it with `unwrap_or_else` to render any `syn::Error` that might happen during derivation. Here's a copy-pastable snippet:
```rust
#[proc_macro_derive(Shareable, attributes(shareable))]
pub fn derive_shareable(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    let ast = parse_macro_input!(input as DeriveInput);

    expand_derive_shareable(&ast)
        .unwrap_or_else(syn::Error::into_compile_error)
        .into()
}

fn expand_derive_shareable(ast: &syn::DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
    //--snip--
}
```

> Notice the `proc_macro2::TokenStream` as opposed to `proc_macro::TokenStream`. Types from `proc_macro` can only exist inside procedural macros, which is why `syn` uses `proc_macro2` instead. Keep an eye out for this if you ever come across errors saying you "can't use `TokenStream` and must use `TokenStream` instead".

### Making requests
With Comlink, it's possible to call a function from the webworker and await the return on the main thread. Wrapping message passing instead of implementing an RPC server was a choice, still we can use what we've developed so far to await responses.

Let's define what we want to achieve: the client should have an async function capable of taking a `Shareable` message, sending it to the server, awaiting a `Shareable` response, and returning it.
```rust
// types.rs
#[derive(Debug, Shareable)]
enum ClientRequest {
    Ping
}

#[derive(Debug, Shareable)]
enum ServerResponse {
    Pong
}
```

```rust
// client.rs
async fn request(&self, message: ClientRequest) -> ServerResponse {
    todo!();
}

async fn run(&self) {
    let message = ClientRequest::Ping;
    trace!("client->server: {:?}", message);
    trace!("client<-server: {:?}", self.request(message).await);
}
```

How should we implement `request`? Before sending a message, a listener must be ready to receive a response and to set that up we can call `add_listener` on the sender port. Upon receiving the response it's easy to get back the `ServerResponse` using `try_into` (whose implementation we didn't have to write because `ServerResponse` is `Shareable`). With a listener set up, the client then sends a message to the server, waits for a response, and returns it (without forgetting to clear the listener).

Before looking at the Rust code it's useful to think about how the implementation would look like using pseudo-code and promises. Using JavaScript to write that pseudo-code, it looks like this:
```javascript
async function request(clientMessage) {
    return new Promise((resolve, _reject) => {
        function onResponse(event) {
	    resolve(event.data);
	}
	    
	this.port.addEventListener('message', onResponse, { once: true });
	this.port.postMessage(clientMessage);
	});
}
```

> Small tip: when prototyping, it's a good idea to be aware of optional parameters being ignored. The Rust compiler has that covered, but with JavaScript the `no-unused-vars` lint rule makes that difficult, even if you are willing to do it manually. For example, `new Promise(resolve, _reject) ...` causes a lint error if `_reject` is not used. However, [the lint rule accepts an ignore pattern](https://github.com/voxelstack/atlas/blob/f6b16ab1b759cb7a2a74154198ba12089cfbbb7d/.eslintrc.cjs#L31-L39), and can be made to accept this as long as the unused variable name matches that pattern (`^_`).

[`js_sys` has bindings for `Promise`](https://docs.rs/js-sys/0.3.64/js_sys/struct.Promise.html) and [wasm-bindgen-futures](https://docs.rs/wasm-bindgen-futures/latest/wasm_bindgen_futures/) provides a bridge between JavaScript promises and Rust futures, meaning this can be translated to Rust. Dealing with the reject and resolve closures would be extra mental overhead, and I had already done some tests that involved installing [tokio](https://tokio.rs/), so for the sake of this proof of concept using channels was faster.

```rust
async fn request(&self, message: ClientMessage) -> ServerResponse {
    let (tx, mut rx) = channel::<ServerResponse>(1);

    let listener = self
        .port
        .add_listener(Closure::new(move |event: MessageEvent| {
            let payload: ServerResponse = event.data().try_into().unwrap();

            tx.try_send(payload.message)
                .expect("Request channel should not be closed.");
        }));

    self.pipe.send(message);

    let response = rx.recv().await.expect("Should post a valid response.");
    listener.clear();

    response
}
```

It almost works, but if the client makes multiple concurrent requests it will have no way of associating them with one of the incoming responses. That issue can be solved by [including a random request id](https://github.com/voxelstack/atlas/blob/f6b16ab1b759cb7a2a74154198ba12089cfbbb7d/src/atlas/client/src/lib.rs#L99-L122) that [the server copies over to the response](https://github.com/voxelstack/atlas/blob/f6b16ab1b759cb7a2a74154198ba12089cfbbb7d/src/atlas/server/src/lib.rs#L35-L81).

## The tally counter
[Comlink uses a counter as an example](https://github.com/GoogleChromeLabs/comlink#running-a-simple-function), and we are now ready to replicate that. The client should be able to:
- query the current value;
- increment or decrement the value;
- set the value.

An enum can represent those operations:
```rust
// types.rs
#[derive(Debug, Shareable)]
pub enum ClientMessage {
    Query,
    Inc,
    Dec,
    Set(#[shareable(repr = "serde")] u8),
}
```

The client still needs some glue code that has to be written manually (this impl block could also be generated automatically using derive macros).
```rust
// client.rs
impl Client {
    pub async fn query(&self) -> u8 {
        self.request(ClientMessage::Query).await
    }
    pub async fn inc(&self) -> u8 {
        self.request(ClientMessage::Inc).await
    }
    pub async fn dec(&self) -> u8 {
        self.request(ClientMessage::Dec).await
    }
    pub async fn set(&self, value: u8) -> u8 {
        self.request(ClientMessage::Set(value)).await
    }
}
```

The server then must store a value and either return or mutate it depending on the request. I am leaving out the details on [how the server receives, processes, and responds to messages](https://github.com/voxelstack/atlas/blob/f6b16ab1b759cb7a2a74154198ba12089cfbbb7d/src/atlas/server/src/lib.rs#L35-L81) (where I also used tokio channels) so we can focus on responding to a request:
```rust
let response = match request{
    ClientMessage::Query => {
        ServerMessage::Query(self.counter)
    }
    ClientMessage::Inc => {
        self.counter += 1;
        ServerMessage::Query(self.counter)
    }
    ClientMessage::Dec => {
        self.counter -= 1;
        ServerMessage::Query(self.counter)
    }
    ClientMessage::Set(value) => {
        self.counter = value;
        ServerMessage.Query(self.counter)
    }
};
```

After all this work, we can finally write some Svelte and create a working demo. After initializing wasm-bindgen on the main thread, instantiate the web worker and give its handle to the client, which allows us to perform requests. The worker needs to initialize wasm-bindgen and the rayon thread pool. For that reason, creating the worker is done through a [utility function](https://github.com/voxelstack/atlas/blob/f6b16ab1b759cb7a2a74154198ba12089cfbbb7d/src/lib/spawner.ts#L1-L14) returning a promise that only resolves after the worker initialization finishes.

```react
<script lang="ts">
    //--snip--
    let client: Client;

    onMount(async () => {
        await initWasm();
        
        // spawn returns a promise that resolves after the worker
        // has finished initializing wasm-bindgen and rayon.
        const server = await spawn(Worker);
        client = new Client(server);
    });
</script>

<button on:click={client.dec}>-</button>
<button on:click={async () => console.log(await client.query())}>GET</button>
<button on:click={client.inc}>+</button>
```

Addition and subtraction without blocking the main thread! More importantly, no need to think about Rust when writing JavaScript, and no need to think about JavaScript when writing Rust.

### Finishing touches
It would be nice to show the value on the DOM instead of the console, and we could already do that by manually updating a DOM node whenever `dec`, `inc`, or `set` are called. Using any modern web framework in the past couple of years will tell you that there is a better way of doing that. Svelte [automatically updates the DOM when a component's state changes](https://learn.svelte.dev/tutorial/reactive-declarations), and ideally we want to be able to write `<span>{counter}</span>` and call it a day.

Before this experiment, I did some tests with wasm-bindgen running on the main thread of a React application. The same issue showed itself, and the solution wasn't trivial: React can't see state changes inside a wasm-bindgen object because the reference to that object stays the same. The first solution that came to mind was to give Rust a function that could run whenever a state change happened and update React state from that function.
```react
function Counter() {
    const [wasmState, setWasmState] = useState({ counter: 0 });
    
    function onChange(wasmStateDiff) {
        setWasmState((previousState) => ({
            ...previousState,
            ...wasmStateDiff,
        }));
    }

    const _wasmCounter = useRef(new WasmCounter(0, onChange));

    return <span>{wasmState.counter}</span>
}
```
> You can imagine WasmCounter calling `onChange({ counter: newValue })` whenever it changes the counter value.

I didn't put a lot of thought or research time into this at the time, but it worked so now with Svelte I started by considering a similar approach. Things looked much worse though: now the data lives on a web worker and is always at least one `postMessage` away.

That was when I came across a seemingly unrelated [article about using RxJs with Svelte](https://timdeschryver.dev/blog/unlocking-reactivity-with-svelte-and-rxjs). It is a good look into how Svelte reactivity works and very much worth a read, but here is the main takeaway: an RxJs Observable serves as a Svelte store (and by extension, so does any observable).

#### A way to describe observables
> For completeness, I want to briefly explain how observables are used here. Check [the RxJs docs on observables](https://rxjs.dev/guide/observable) for a proper description.

For our purposes, an observable is something that can notify you about changes in a value. To tell the observable you want to be notified you subscribe to it, and to subscribe you provide a callback that will receive those notifications. Subscribing also gives you an unsubscribe function for when you want to stop observing the value. Some code:

```javascript
let counter = new Observable(0);
setInterval(() => counter.set(counter.get() + 1), 1000);

const unsubscribe = counter.subscribe(
    (newValue) => console.log(`counter changed to: ${newValue}`)
);

//--snip--

// Stop observing the value.
unsubscribe();
```
#### Server events
We still cannot use this knowledge unless the server has a way to notify the client of value changes. In practice, all we need is for the server to post a message with some sort of event descriptor (WebSockets are a good analogy for this).
```rust
#[derive(Debug, Shareable)]
pub enum ServerEvent {
    Count(#[shareable(repr = "serde")] u8)
}
```

Next, there should be a way to subscribe to these events and since there might be many types of events, subscribing needs an ID specifying which event we are subscribing to. I used the enum name and variant name separated by `::`, since that matches how you would describe the Rust type. For example, to subscribe to the counter change events, you would use `ServerEvent :: Count` as the id.

> The spaces before and after `::` are to match Rust's [`stringify!`](https://doc.rust-lang.org/std/macro.stringify.html): a macro that turns a list of tokens into a `& 'static str`.

```rust
// client.rs
    //--snip--
    pub fn observe(&mut self, id: String) -> Observable {
        todo!();
    }
```

Like we did with the requests, before talking about [the Rust implementation](https://github.com/voxelstack/atlas/blob/f6b16ab1b759cb7a2a74154198ba12089cfbbb7d/src/atlas/client/src/lib.rs#L142-L169) let's see how we'd implement an observable for these server events using pseudo code written in JavaScript.

The observable needs to store the ID of the events it cares about and a port that receives events. When subscribe is called, it gets a function that will be used to notify the subscriber of changes. That function has to be stored and called whenever the value changes. Finally, it needs to return an unsubscribe function. It looks like this:
```javascript
class Observable {
    constructor(id, port) {
        this.id = id;
        this.port = port;
        this.listeners = [];

        this.port.addEventListener('message', (event) => {
            if (event.data.id === this.id) {
                listeners.forEach(onChange => onChange(event.data.newValue))
            }
        })
    }
    
    subscribe(onChange) {
        this.listeners.push(onChange)

        function unsubscribe() {
            this.listeners = this.listeners.filter(l => l !== onChange)
        }
        return unsubscribe;
    }
}
```

> Notice how `onChange` will not be called when we first subscribe, meaning the subscriber won't know the current value unless it changes. That can be fixed by either querying the current value whenever a new subscriber is added or keeping a local copy of the value on the observable. I went with the first solution, which is the simplest.

Here is the Rust API:
```rust
impl Observable {
    pub fn subscribe(&mut self, on_change: js_sys::Function) -> JsValue {
    //--snip--
}
```

> Helpful tip: to return a wasm-bindgen closure to JavaScript and transfer the memory management responsibility from Rust to the JavaScript garbage collector, call [`into_js_value`](https://docs.rs/wasm-bindgen/latest/wasm_bindgen/closure/struct.Closure.html#method.into_js_value).

##### The hacky part
I didn't talk about the Rust implementation of an observable, and the reason for that is simple: it's hacky.

By this point, I had already achieved my initial goal but only had a boring console demo to show for it. Observables are something I added later to put together a more interesting demo. That was when I came across one of the most practical ideas of this entire project (reactivity with Svelte + WebAssembly). So instead of skipping it I will quickly go over why this part of the implementation is hacky.

An observable should be able to hold a reference to a port and receive events through there. That would require the observable to have a lifetime parameter, but structs shared with JavaScript cannot have lifetime or type parameters (a wasm-bindgen limitation). Cloning the port would solve this, but the port holds a `Box<dyn RawPort>` which is not `Clone`. Another solution is to transmute away the lifetime, and I wanted to avoid doing that.

To get around this, I decided to create a `MessageChannel` that receives the server events and then forwards then to a `BroadcastChannel`, which can easily be accessed from anywhere using just a string ID. This was also an opportunity to [implement `RawPort` for a new type](https://github.com/voxelstack/atlas/blob/f6b16ab1b759cb7a2a74154198ba12089cfbbb7d/src/atlas/comms/src/port.rs#L58-L82).

> In the end, it was unavoidable to [transmute away a lifetime](https://github.com/voxelstack/atlas/blob/f6b16ab1b759cb7a2a74154198ba12089cfbbb7d/src/atlas/client/src/lib.rs#L65-L73) so I could keep a handle to this forwarding listener. By "transmuting away a lifetime" I mean changing the lifetime to `'static`. Very rarely will that be the correct thing to do, and if done incorrectly it will leak memory. So don't do it (unless you're writing a demo).

### Putting it all together
That was quite the journey for a tally counter, but all the pieces are in place now.

> There is [an issue with Vite that prevents me from building this](https://github.com/vitejs/vite/issues/13367), so sadly I cannot give you a link with the working counter. I encourage you to clone the repo and play around with it, [building and running this is surprisingly easy](https://github.com/voxelstack/atlas#running).

Let's start by describing what we want to achieve:
- the current value of the counter should be shown on the DOM;
- there should be buttons to increment and decrement the counter;
- you should be able to subscribe and unsubscribe from a logger, which prints every counter change to the console.

And then we can put it all together: the +/- buttons just need to call the corresponding client functions. To show the value on the DOM, we create an observable from the `ServerEvent::Count` events. And finally, we subscribe to that observable with a function that logs changes to the console, storing the unsubscribe function for later.

```react
<script lang="ts">
    //--snip--    
    let client: Client;
    let count: Observable;
    let unsubscribe: Function | undefined;

    const logger = (value: number) => console.log(`observed: ${value}`);

    onMount(async () => {
        await initWasm();
        
        // spawn returns a promise that resolves after the worker
        // has finished initializing wasm-bindgen and rayon.
        const server = await spawn(Worker);
        client = new Client(server);

        count = client.observe('ServerEvent :: Count');
        unsubscribe = count.subscribe(logger)
    });
</script>

<button on:click={client.dec}>-</button>
<span>{$count}</span>
<button on:click={client.inc}>+</button>

<button
    on:click={() => {
        unsubscribe?.();
        unsubscribe = undefined;
    }}
>detach logger</button>
<button
    on:click={() => {
        unsubscribe = count.subscribe(logger);
    }}
>attach logger</button>
```

![A recording of a website with the finished tally counter. It shows the current value, a button with a minus sign to the left, and a button with a plus sign to the right. Under the  value are buttons called "detach logger" and "attach logger".](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ljnkg2cyh13w4kn1zby7.gif)

Let's take a moment to appreciate this line: `<span>{$count}</span>`. `count` lives on our server: Rust code running on a web worker. And yet whenever it changes, this span updates. Clicking a button will use a Rust client running on the main thread to post a message, which tells a Rust server running on a web worker to mutate some state. After the mutation, the server will generate a change event, post a message to the client, and update the DOM. I hope you find that as amazing (and useful) as I do.