Skip to content

Commit b831e95

Browse files
authored
Merge pull request #46 from fitzgen/gloo-update-onion-layers
Add "Gloo Update: Onion Layers, Timers, and Events"
2 parents dcd2d85 + e6a3451 commit b831e95

File tree

1 file changed

+294
-0
lines changed

1 file changed

+294
-0
lines changed
Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
---
2+
title: "Gloo Update: Onion Layers, Timers, and Events"
3+
layout: "post"
4+
author: "Nick Fitzgerald"
5+
---
6+
7+
About two weeks ago, we [kicked off][lets-build-gloo] our effort to collectively
8+
build [Gloo][], a modular toolkit for building fast and reliable Web apps and
9+
libraries with Rust and Wasm. We knew we wanted to explicitly cultivate the Rust
10+
and Wasm library ecosystem by spinning out reusable, standalone libraries:
11+
things that would help you out whether you were writing a green-field Web app in
12+
pure-Rust, building your own framework, or surgically inserting some
13+
Rust-generated Wasm into an existing JavaScript project. What was still fuzzy,
14+
and which we didn't know yet, was *how* we were going design and expose these
15+
reusable bits.
16+
17+
## Onion-Layered APIs
18+
19+
I'm pleased to tell you that that after some collaborative discussion in issue
20+
threads, we've come up with a promising approach to designing Gloo APIs, and
21+
we've since formalized it a bit in `CONTRIBUTING.md`. I've nicknamed this
22+
approach "onion-layered" API design.
23+
24+
Briefly, we want to build mid-level abstraction libraries on top of raw `-sys`
25+
bindings, build futures and streams integration on top of the mid-level APIs,
26+
and build high-level APIs on top of all that. But — crucially —
27+
every layer should be publicly exposed and reusable.
28+
29+
While this approach to API design is certainly not novel, we want to very
30+
deliberately follow it so that we
31+
32+
* maximize reusability for the larger ecosystem, and
33+
* exercise our mid-level APIs when building higher-level APIs, to ensure their
34+
generality and suitability for acting as a solid foundation.
35+
36+
As we go through and examine each layer, I'll use [the `setTimeout` and
37+
`setInterval` Web APIs][set-timeout] as a running example.
38+
39+
## The Core: `wasm-bindgen`, `js-sys`, and `web-sys`
40+
41+
The innermost layer are raw bindings built on top of [`wasm-bindgen`, `js-sys`
42+
and `web-sys`][announcing-web-sys]. These bindings are fast, have a light code
43+
size foot print, and are future-compatible with [the host bindings
44+
proposal][host-bindings].
45+
46+
What they are *not* is super ergonomic to use all of the time. Using raw
47+
`web-sys` bindings directly can sometimes feel like making raw `libc` calls
48+
instead of leveraging Rust's nice `std` abstractions.
49+
50+
Here is doing some operation after a 500 millisecond timeout using raw `web-sys`
51+
bindings:
52+
53+
```rust
54+
use wasm_bindgen::{closure::Closure, JsCast};
55+
56+
// Create a Rust `FnOnce` closure that is exposed to JavaScript.
57+
let closure = Closure::once(move || {
58+
do_some_operation();
59+
});
60+
61+
// Get the JavaScript function that reflects our Rust closure.
62+
let js_val = closure.as_ref();
63+
let js_func = js_val.unchecked_ref::<js_sys::Function>();
64+
65+
// Finally, call the `window.setTimeout` API.
66+
let id = web_sys::window()
67+
.expect("should have a `window`")
68+
.set_timeout_with_callback_and_timeout_and_arguments_0(js_func, 500)
69+
.expect("should set a timeout OK");
70+
71+
// Then, if we ever decide we want to cancel the timeout, we do this:
72+
web_sys::window()
73+
.expect("should have a `window`)
74+
.clear_timeout_with_handle(timeout_id);
75+
```
76+
77+
## The `callbacks` Layer
78+
79+
When we look at the raw `web-sys` usage, there is a bit of type conversion
80+
noise, some unfortunate method names, and a handful of `unwrap`s for ignoring
81+
edge-case scenarios where we prefer to fail loudly rather than limp along. We
82+
can clean all these things up with the first of our "mid-level" API layers,
83+
which in the case of timers is the `callbacks` module in the `gloo_timers` crate
84+
(which is also re-exported from the `gloo` umbrella crate as `gloo::timers`).
85+
86+
The first "mid-level" API built on top of the `-sys` bindings exposes all the
87+
same functionality and the same design that the Web does, but uses proper Rust
88+
types. For example, at this layer, instead of taking untyped JavaScript
89+
functions with `js_sys::Function`, we take any `F: FnOnce()`. This layer is
90+
essentially the least opinionated direct API translation to Rust.
91+
92+
```rust
93+
use gloo::timers::callbacks::Timeout;
94+
// Alternatively, we could use the `gloo_timers` crate without the rest of Gloo:
95+
// use gloo_timers::callbacks::Timeout;
96+
97+
// Already, much nicer!
98+
let timeout = Timeout::new(500, move || {
99+
do_some_operation();
100+
});
101+
102+
// If we ever decide we want to cancel our delayed operation, all we do is drop
103+
// the `timeout` now:
104+
drop(timeout);
105+
106+
// Or if we never want to cancel, we can use `forget`:
107+
timeout.forget();
108+
```
109+
110+
## Layering on Futures and Streams
111+
112+
The next layer to add is integrating with popular traits and libraries in the
113+
Rust ecosystem, like `Future`s or `serde`. For our running `gloo::timers`
114+
example, this means we implement a `Future` backed by `setTimeout`, and a
115+
`Stream` implementation backed by `setInterval`.
116+
117+
```rust
118+
use futures::prelude::*;
119+
use gloo::timers::futures::TimeoutFuture;
120+
121+
// By using futures, we can use all the future combinator methods to build up a
122+
// description of some asynchronous task.
123+
let my_future = TimeoutFuture::new(500)
124+
.and_then(|_| {
125+
// Do some operation after 500 milliseconds...
126+
do_some_operation();
127+
128+
// and then wait another 500 milliseconds...
129+
TimeoutFuture::new(500)
130+
})
131+
.map(|_| {
132+
// after which we do another operation!
133+
do_another_operation();
134+
})
135+
.map_err(|err| {
136+
handle_error(err);
137+
});
138+
139+
// Spawn our future to run it!
140+
wasm_bindgen_futures::spawn_local(my_future);
141+
```
142+
143+
Note that we use `futures` 0.1 for now, because we've fought tooth and nail to
144+
get the Wasm ecosystem on stable Rust, but as soon as the new
145+
`std::future::Future` design is stable, we plan to switch over. We are very
146+
excited for `async`/`await` as well!
147+
148+
## More Layers?
149+
150+
That's all the layers we have for the `setTimeout` and `setInterval`
151+
APIs. Different Web APIs will have different sets of layers, and this is
152+
fine. Not every Web API uses callbacks, so it doesn't make sense to always have
153+
a `callbacks` module in every Gloo crate. The important part is that we are
154+
actively identifying layers, making them public and reusable, and building
155+
higher-level layers on top of lower-level layers.
156+
157+
We will likely add even higher-level layers to other Web APIs where it makes
158+
sense. For example, the [File API][]'s `FileReader` interface exposes methods
159+
that you shouldn't call until after certain events have fired, and any attempt
160+
to call them earlier will throw. We can codify this as [a state machine-based
161+
`Future`][state-machine-future], that doesn't even give you the ability to call
162+
those methods until after the relevant events have fired and the state machine
163+
reaches a certain state. Leveraging types at compile time for ergonomics and
164+
correctness!
165+
166+
Another future direction is adding more integration layers with more parts of
167+
the larger Rust crates ecosystem. For example, adding functional reactive
168+
programming-style layers via [the `futures-signals`
169+
crate][integrate-futures-signals] which is also used by the
170+
[`dominator`][dominator] framework.
171+
172+
## Events
173+
174+
One of the active bits of design work going on in Gloo right now is how to craft
175+
our event targets and listeners layer. Events are used across most of the Web
176+
APIs, so it is very important we get this design right, as it will sit
177+
underneath many of our other crates. While we haven't 100% nailed down the
178+
design yet, I really like where we are headed.
179+
180+
On top of [`web_sys::Event`][web-sys-event] and
181+
[`web_sys::EventTarget::add_event_listener_with_callback`][web-sys-add-listener],
182+
we are building a layer for [adding and removing event
183+
listeners][raii-listeners] and managing their lifetimes from Rust via RAII-style
184+
automatic cleanup upon drop.
185+
186+
We can use this API to make idiomatic Rust types that attach event listeners
187+
that automatically get removed from the DOM when the type is dropped:
188+
189+
```rust
190+
use futures::sync::oneshot;
191+
use gloo::events::EventListener;
192+
193+
// A prompt for the user.
194+
pub struct Prompt {
195+
receiver: oneshot::Receiver<String>,
196+
197+
// Automatically removed from the DOM on drop!
198+
listener: EventListener,
199+
}
200+
201+
impl Prompt {
202+
pub fn new() -> Prompt {
203+
// Create an `<input>` to prompt the user for something and attach it to the DOM.
204+
let input: web_sys::HtmlInputElement = unimplemented!();
205+
206+
// Create a oneshot channel for sending/receiving the user's input.
207+
let (sender, receiver) = oneshot::channel();
208+
209+
// Attach an event listener to the input element.
210+
let listener = EventListener::new(&input, "input", move |_event: &web_sys::Event| {
211+
// Get the input element's value.
212+
let value = input.value();
213+
214+
// Send the input value over the oneshot channel.
215+
sender.send(value)
216+
.expect_throw(
217+
"receiver should not be dropped without first removing DOM listener"
218+
);
219+
});
220+
221+
Prompt {
222+
receiver,
223+
listener,
224+
}
225+
}
226+
}
227+
228+
// A `Prompt` is also a future, that resolves after the user input!
229+
impl Future for Prompt {
230+
type Item = String;
231+
type Error = ();
232+
233+
fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
234+
self.receiver
235+
.poll()
236+
.map_err(|_| {
237+
unreachable!(
238+
"we don't drop the sender without either sending a value or dropping the whole Prompt"
239+
)
240+
})
241+
}
242+
}
243+
```
244+
245+
On top of that layer, we are using Rust's trait system to design [a
246+
higher-level, static events API][static-events] that will make the events
247+
casting safe and statically-checked, and make sure you don't have typos in the
248+
event types that you listen to:
249+
250+
```rust
251+
use gloo::events::{ClickEvent, on};
252+
253+
// Get an event target from somewhere.
254+
let target: web_sys::EventTarget = unimplemented!();
255+
256+
// Listen to the "click" event, know that you didn't misspell the event as
257+
// "clik", and also get a nicer event type!
258+
let click_listener = on(&target, move |e: &ClickEvent| {
259+
// The `ClickEvent` type has nice getters for the `MouseEvent` that
260+
// `"click"` events are guaranteed to yield. No need to dynamically cast
261+
// an `Event` to a `MouseEvent`.
262+
let (x, y) = event.mouse_position();
263+
264+
// ...
265+
});
266+
```
267+
268+
These event APIs are still works in progress and have some kinks to work out,
269+
but I'm very excited for them, and we hope to get a lot of mileage out of them
270+
as we build other Gloo crates that internally use them.
271+
272+
## Get Involved!
273+
274+
Let's build Gloo together! Want to get involved?
275+
276+
* [Join the `#WG-wasm` channel on the Rust Discord server!][discord]
277+
* [Follow the `rustwasm/gloo` repository on GitHub and check out its
278+
`CONTRIBUTING.md`][Gloo]
279+
280+
281+
[lets-build-gloo]: https://rustwasm.github.io/2019/03/12/lets-build-gloo-together.html
282+
[Gloo]: https://github.com/rustwasm/gloo
283+
[set-timeout]: https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout
284+
[announcing-web-sys]: https://rustwasm.github.io/2018/09/26/announcing-web-sys.html
285+
[host-bindings]: https://github.com/WebAssembly/host-bindings/blob/master/proposals/host-bindings/Overview.md
286+
[state-machine-future]: https://github.com/fitzgen/state_machine_future
287+
[integrate-futures-signals]: https://github.com/rustwasm/gloo/issues/33
288+
[dominator]: https://github.com/Pauan/rust-dominator
289+
[File API]: https://github.com/rustwasm/gloo/issues/47
290+
[web-sys-add-listener]: https://docs.rs/web-sys/0.3.17/web_sys/struct.EventTarget.html#method.add_event_listener_with_callback
291+
[web-sys-event]: https://docs.rs/web-sys/0.3.17/web_sys/struct.Event.html
292+
[raii-listeners]: https://github.com/rustwasm/gloo/issues/30
293+
[static-events]: https://github.com/rustwasm/gloo/issues/43
294+
[discord]: https://discord.gg/rust-lang

0 commit comments

Comments
 (0)