Giter VIP home page Giter VIP logo

gloo's People

Contributors

aehmlo avatar andoriyu avatar b0xtch avatar cbrevik avatar dakom avatar david-oconnor avatar daxpedda avatar dependabot[bot] avatar derekdreery avatar fitzgen avatar flisky avatar flosse avatar futursolo avatar hseeberger avatar huntc avatar jplatte avatar langyo avatar lnicola avatar madoshakalaka avatar oddcoincidence avatar pauan avatar philippgackstatter avatar prockallsyms avatar ranile avatar rylev avatar samcday avatar simbleau avatar striezel avatar worldsender avatar xfbs avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

gloo's Issues

Proposal: gloo-properties - mid-level API for getting and setting properties

Summary

A mid-level API to set and query properties on arbitrary Javascript values.

Motivation

The js-sys crate already provides Reflect to get and set properties on JsValue types, and wasm-bindgen provides Serde integration that allows to easily map an existing Rust type into Javascript land. So why do we need more?

The use case we're addressing here is ad-hoc property access. This occurs often when writing Rust WASM code that interfaces with existing userland Javascript APIs. It's expected that as the Rust WASM ecosystem matures, this situation will decline in frequency. However that kind of shift is not going to happen overnight, and in the meantime we should provide better ergonomics for users that need to interface with raw Javascript values.

Detailed Explanation

gloo_properties::property

Used to get/set a single property on an existing JsValue.

API:

/// Retrieves a single Property on target Object. This Property can be used to
/// get and set the value.
pub fn property<K>(target: &JsValue, key: K) -> Property
    where K: Into<JsValue> { /* ... */ }

struct Property { /* ... */ }
impl Property {
    /// Retrieves the value of this Property as a String. If the property is
    /// undefined, null, or not a String, an Err is returned.
    pub fn string(&self) -> Result<String, JsValue> { /* ... */ }

    /// Retrieves the value of this Property as a u32. If the property is
    /// undefined, null, not a Number, or overflows a u32, an Err is returned.
    pub fn u32(&self) -> Result<u32, JsValue> { /* ... */ }
    // And so on...
    pub fn u16(&self) -> Result<u16, JsValue> { /* ... */ }
    // pub fn u8...
    // pub fn i32...
    // pub fn i16...
    // pub fn i8...

    /// Retrieves the value of this Property as a bool. If the property is
    /// undefined, null, or not a Boolean, an Err is returned.
    pub fn bool(&self) -> Result<bool, JsValue> { /* ... */ }

    /// Retrieves the value of this Property as any kind of Javascript mapped
    /// type. Anything that is exported from #[wasm_bindgen] is usable. If the
    /// property is undefined, null, or an incorrect type, an Err is returned.
    pub fn cast<T: JsCast>(&self) -> Result<T, JsValue> { /* ... */ }

    /// Retrieves a sub-property from this Property. This is used to traverse
    /// arbitrary nested object structures. Note that if a property does not
    /// exist, a Property will still be returned, but any attempts to get or set
    /// a value will return an Error.
    pub fn property<K: Into<JsValue>>(self, key: K) -> Property { /* ... */ }

    /// Sets the given value on the Property. Returns an error if the set failed
    /// for any reason. e.g frozen object, error in a setter on the Object, etc.
    pub fn set<V: Into<JsValue>>(&self, val: V) -> Result<bool, JsValue> { /* ... */ }
}

Usage examples:

use gloo_properties::property;

// Because key is generic over Into<JsValue>, using it is very convenient:
property(&obj, "foo");           // static string literals
property(&obj, &existing_str);   // existing String / &strs
property(&obj, existing_str);    // Pass by ref not required.

// Symbols work too.
let sym = JsValue::symbol("secret");
property(&obj, &sym);

// Nested property access:
property(&obj, "foo").property("bar").property("quux");

// Retrieving values.
property(&obj, "foo").string().unwrap_throw();
let my_element: web_sys::Element = property(&obj, "foo").cast();

// Traversing multiple levels never fails until trying to get/set something.
property(&obj, "i").property("don't").property("exist"); // -> Property
property(&obj, "exists").property("nope").string() // -> Err("Property 'i' does not exist")

// Setter is generic over anything that can convert into a JsValue, which is
// all core Rust types, and all web_sys types.
property(&obj, "foo").set("a direct string");
property(&obj, "foo").set(42u32);
property(&obj, "foo").set(true);

let an_existing_object = Object::new();
property(&obj, "foo").set(an_existing_object);

gloo_properties::setter

Used to quickly build up a set of properties on a value.

API:

/// Returns a Setter that can be used to set multiple properties on the given
/// target. Ownership of the target is taken until Setter#finish() is called.
pub fn setter<T: JsCast>(target: T) -> Setter<T> { /* ... */ }

pub struct Setter<T: JsCast> { /* ... */ }
impl<T: JsCast> Setter<T> {
    /// Sets given key/value pair on the target object.
    pub fn set<K, V>(self, key: K, val: V) -> Self
    where
        K: Into<JsValue>,
        V: Into<JsValue>,
    { /* ... */ }

    /// Sets given key/value pair on the target object, and then calls the given
    /// closure to set sub properties on the provided value.
    pub fn with<K, V, F>(self, key: K, val: V, func: F) -> Self
    where
        K: Into<JsValue>,
        V: JsCast,
        F: FnOnce(Setter<V>) -> Setter<V>,
    { /* ... */ }

    /// Finishes setting properties on target object and returns it.
    pub fn finish() -> T { /* ... */ }
}

Usage:

use gloo_properties::setter;

let my_sym = JsValue::symbol("secret");

let my_obj = setter(Object::new())
    .set("foo", "bar")
    .set(&my_sym, "quux")
    .set(123, 321)
    .set("bool", false)
    .set("jsval", Object::new())
    .with("nested", Object::new(), |sub| sub
        .set("and", "so")
        .set("on", "it goes"))
    .finish();

Drawbacks, Rationale, and Alternatives

Drawbacks

Currently, working with Reflect (which this crate would use) is slower than working with duck-typed interfaces, and in many cases is also slower than the Serde integration.

Alternatives

The obvious alternatives are to use duck-typed interfaces or the Serde integration. Both of these approaches require one to write and annotate a struct with the desired fields. The proposed API is suitable for use when the user does not wish to define explicit types in order to grab properties out of an existing value in an ad-hoc manner.

For example:

// Instead of needing to do this:
{
    #[wasm_bindgen]
    extern "C" {
        type MyThing;

        #[wasm_bindgen(method, getter, structural)]
        fn foo(this: &MyThing) -> String;
    }
    let thing: MyThing = my_obj.dyn_into::<MyThing>().unwrap_throw();
    thing.foo
}

// Or this:
{
    #[derive(Deserialize)]
    struct MyThing {
        foo: String,
    }
    let thing: MyThing = JsValue::into_serde(&my_obj).unwrap_throw();
    thing.foo
}

// One can just do this:
{
    property(&my_obj, "foo").string().unwrap_throw();
}

Unresolved Questions

Currently, none.

XmlHttpRequest (mid-level only)

Summary

Provide "mid-level" bindings to the XMLHttpRequest API. "mid-level" in this case would mean a callback-based API to cover the whole of XHR, mirroring web_sys::XmlHttpRequest with more idiomatic Rust types and ergonomic improvements where possible without compromising fidelity to the original.

Motivation

The "blessed" browser API for HTTP going forward is fetch, but fetch does not provide all the features XMLHttpRequest does at the moment (one notable example being download/upload progress events).

This could also be used as a base for higher level bindings providing a more ergonomic interface from Rust, but I propose leaving it to a future discussion.

Detailed Explanation

The bindings would be implemented inside a new gloo-xhr crate in a callback top-level module, to leave room for a potential future-based wrapper built on of it.

Two examples to motivate why we would benefit from "mid-level" bindings, compared to raw web_sys:

  • Callbacks would be typed Rust closures.
  • We wouldn't have many methods to set the body of a request, like in web_sys, but a set_body method generic over a (for example) XhrRequestBody trait implemented by all valid body types.

The documentation should mention that fetch is the API that is currently being pushed forward in browsers and will eventually supersede XmlHttpRequest.

Drawbacks, Rationale, and Alternatives

Alternatively, these bindings could be grouped with the fetch bindings in a common gloo-http or gloo-ajax crate.

The implementation would aim to map as close as possible to the original web API; as such few design decisions should be necessary.

Unresolved Questions

Providing nice, ideally typed errors could be challenging, as the error cases in networking APIs are many.

Get headless browser tests running on Safari in CI

Should be similar to our existing wasm-pack tests that run in firefox and chrome, but we need to use a macos vm image, and I'm not 100% sure how to do that in azure pipelines yet!

See .azure-pipelines.yml and .ci/wasm-pack-test-headless.yml for more details.

What do we want in an initial Gloo 0.1 release?

I'd like to open this issue to discuss what we want in an initial Gloo release.

We will want to have resolved our plan for versioning Gloo before we actually cut any releases. I think this is the only hard requirement.

We have a spectrum along which to choose: on one end we have "deliver some tiny value to users very soon" and on the other end we have "deliver bigger value to users later".

The more stuff we put in the 0.1 release, the more of a "wow"-factor there is with the amount of things we are shipping, but the longer it will take to be ready. The fewer stuff we put in the 0.1 release, the sooner it starts helping users and we start getting real world feedback on Gloo, but the less of a "wow"-factor the release will have.


I propose that we find a subset of the in-flight proposals that we would like to focus on for an 0.1 release. Reach consensus on their designs, get them implemented, and ship an 0.1.

I think this subset should be fairly small and conservative, so we can ship on the sooner end of the spectrum. Because we are doing so much design work up front, the implementation of each proposal is actually a fairly complete API that has more bang for its buck in terms of "wow"-factor and value for users than an equivalently-sized PR otherwise might.

Note that whatever we choose as blockers for the initial release, anything else that lands while we are still trying to finish the blockers would also make it in that 0.1 release too. But those things would be nice-to-haves and we wouldn't delay the release for them.

Here is what I propose as the 0.1 release blockers:

  • gloo_timers โ€” already implemented \o/
  • gloo_console_timer โ€” already implemented \o/
  • gloo_events โ€” already implemented \o/
    • But we still need to have its callbacks take events by reference rather than value (TODO: file an issue for this #82)
  • gloo_file โ€” relatively self-contained and I think we are very close to finalizing the design.
  • gloo_(properties|reflect) โ€” again, this seems pretty close and there seem to only be two relatively minor open questions about the design still.
  • Create a compelling example for the README that uses at least two different Gloo crates together. (TODO: file an issue for this #81)
  • Fill out the Gloo guide mdbook a bit (TODO: file an issue for this #80)

This list is totally up for discussion and we can add and remove items from it if we want. What do y'all think?

Thoughts on the Direction of Gloo

Thoughts on the Direction of Gloo

Let me first start by saying that @rustwasm has changed my programmer-life for the better in more ways than I can count. I'm absolutely loving the Rust + WebAssembly story!

I say that so that anything that I say below doesn't sound like dissent. I just want to share feedback which will hopefully help push @rustwasm forwards!

I recognize that some of my thoughts will be wrong and/or misinformed - so feel free to point that out!!!

Also big disclaimer that I maintain some Rust web front-end tools although I'm hoping that that doesn't make my feedback too biased!


wasm-bindgen wasm-pack and the rest of rustwasm are so successful because they provide functionality / solve pain points in a way that is pretty much objectively correct.

You're unlikely to see a second wasm-bindgen or a second wasm-pack because they aren't trying to be much more than exactly what almost anyone would want on the low level.

Just about anyone that needs something that doesn't exist will contribute it back to wasm-bindgen or wasm-pack if it's low level - or just build in top of wasm-bindgen or wasm-pack.

They do an excellent job of focusing on problems that are solvable almost entirely without any material disagreement - and letting the community build on top of that.


I'm very afraid of Gloo breaking this mold.

I'm getting the impression that we might be heading in the direction of "Come to Gloo to get modules that you need."

This to me will just lead to "Gloo has some great modules, but there are 14 other people building similar modules because they disagree with how Gloo did it."


I think that a better direction would be for Gloo to focus on being the glue between modules that the community builds.

This would mostly look like Traits and generic methods that ... so long as a community module implements these traits it's compatible with the rest of the ecosystem.

We don't need one toolkit to rule them all IMO. I think we need Gloo to empower dozens of other developers to iterate on what they think Rust web modules should look like.

And the community can very easily try out the options since they all are compatible with these Gloo traits / generic methods.

And all of these maintainers are contributing back to Gloo as they learn.

Rust on the web is still young - so there will certainly be a lot of learning around what patterns allow us to build production grade applications the fastest.

I want to trust the community to discovery these things, and I want to trust @rustwasm to empower the community on that journey by building out the most lowest-common-denominator libraries and tooling.


What I think Gloo should do

Look at what people are already doing and try to be the glue between them.

What do the virtual-dom's look like today? What do the procedural macros for HTML parsing look like today? What do the diff/patch algorithms look like?

Can Gloo provide traits / generic methods that almost every single one of these libraries would build on top of?

Thereby unifying the community and giving people with different ideas the room to explore them while contributing what they find back to the super-low-level Gloo crates?

Which - I know is the goal! I just think that some of the issues that I see are far too high level. So I'll give some examples below of what I mean by low level, least common denominator.

Things I would find immediately useful

Validation for DOM nodes and attributes.

I maintain an html-macro - and it looks like there are a few others in the community - each with their own advantages / disadvantages.

A disadvantage of my macro is that there isn't nearly as much type checking as in typed-html. But I can't switch to typed-html even when they have wasm-bindgen support because I personally prefer not having to wrap text in quotation marks.

It'd be nice if Gloo provided the validation that typed-html html-macro and any other macros could build on top of.

"Is this string a valid class". "Is this href valid?". etc

A VirtualNode Trait

People will have different thoughts and ideas on how a diff/patch algorithm should look.. But one thing that is pretty consistent is that virtual nodes all have properties ... events ... and basically look like real DOM elements.

As people explore diff/patch algorithms and their own virtual dom's it would be nice if they were all building on top of Gloo.

Don't like one diff/patch algorithm that someone provides? Switch to another one pain free since they're both generic on top of Gloo's VirtualNode trait!

I probably have other ideas

I can open issues when they come to mind if useful! But hopefully these were helpful!

In general I think that for a lot of the issues that are open we can zoom in to specific pieces of them that are un-opinionated and focus on that.

In Summary

I think that the best path forwards for Gloo right now for a lot of these issues that are open is to look at unifying/being the glue between all libraries that exist now and will exist in the future.

Gloo should spur innovation by being the foundation for all future experimentation.

Gloo should focus on the bits of the stack and the problems that will have nearly zero opinion on how to do them properly - and let the community build on top of those solutions (just like wasm-bindgen and wasm-pack).


Just my thoughts! Hope it helps!

Standardized documentation

Summary

All Gloo modules should have narrative documentation, perhaps as part of a centralized guide.

Motivation

It's common in the Rust community to rely on API docs, and narrow-scoped examples. These are important, but not sufficient.

Detailed Explanation

With some exceptions, Rust libs, including most WASM frameworks, tend to be documented through examples and API docs. While these are important, having a detailed, narrative guide is important too. For example, the Rust book. I think all Gloo components should have a maintained guide of this style. Could use the Rust Book template. Example: Vulkano has a very extensive API, a guide demonstrating some things, and a few examples, but is missing large chunks of info, like how to use it for 3d items, or multiple objects. Example: Yew is a popular stdweb-based framework, but has no narrative guide outside a brief readme, which until recently, was misaligned with the released API. Example: Rocket is a backend framework with a detailed guide.

Related: In addition to feature-specific examples, and long/complex ones, I think short ones that demonstrate API coverage, and using various features together are useful... and missing in many Rust crates.

Drawbacks, Rationale, and Alternatives

It requires more upfront effort, and maintenance work when APIs change.

Doing this would make Gloo more appealing to people who are experienced with frontend dev, but not Rust.

App state management

By this I mean things like

  • Redux stores and actions
  • Elm models and messages
  • Etc...

Another opinionated puzzle piece!

How deeply should we integrate with the ecosystem?

Right now web-sys provides very low-level access to the web APIs. My understanding is that Gloo is intended to create idiomatic high-level Rust APIs on top of those low-level APIs. However, there are different degrees of "high-level".

As an example, it's easy to wrap setTimeout and setInterval to make a nice convenient API that uses FnOnce and FnMut. But that's only slightly more high-level than using the APIs directly.

On the other hand, we could wrap setTimeout and setInterval into a Future and Stream, which is definitely much higher level (and more idiomatic for Rust).

So when we are faced with this choice between mid-level and high-level, what should we choose? I think we should have a consistent approach across the Gloo ecosystem.

I propose that we always expose the high-level APIs (Future, Stream, etc.) and then have a submodule which exposes the mid-level APIs.

That makes it clear that the high-level APIs are the idiomatic APIs that generally should be used, but still makes the mid-level APIs accessible for unusual circumstances.

To start the bikeshedding, I propose we consistently use the name raw for the mid-level submodule.

Set up continuous integration

Thought I already did this but I guess not >.<

I'd like to do Azure pipelines since they have a higher level of parallelism at the free tier than travis ci. Also, they're not recently acquired by a sketchy, strip-it-down-for-parts company that laid off a ton of people.

WebAudio library

Let's make an idiomatic, Rust-y interface to the WebAudio API!

WebExtensions alarms

Summary

Expose an API matching gloo-timers for using alarms in WebExtensions.

Motivation

The MDN documentation describes alarms as "like setTimeout() and setInterval(), except that those functions don't work with background pages that are loaded on demand". Because of these similarities, gloo-timers provides a good precedent for the design of this API.

Detailed Explanation

The gloo-webextensions crate would, like timers, expose both callback-style and futures-based APIs for alarms in submodules called callback and future, the latter gated on the futures feature.

Note: one difference between timers and alarms is that the browser generates ids for timeouts and intervals, but we are responsible for generating a unique name for an alarm.

Callback API skeletons:

pub struct Alarm { .. }

impl Alarm {
    // Create a new alarm that will fire once in `millis` ms
    pub fn new(millis: f64, callback: F) -> Alarm
    where
        F: 'static + FnOnce()
    { .. }

    // returns the generated name for the alarm
    pub fn forget(mut self) -> AlarmId { .. }

    // prevent alarm from firing
    pub fn cancel(mut self) -> Closure<FnOnce()> { .. }
}

// cancels alarm
impl Drop for Alarm { .. }

pub struct RecurringAlarm { .. }

impl RecurringAlarm {
    // create a new alarm that will fire every `minutes` minutes
    pub fn new<F>(minutes: f64, callback: F) -> RecurringAlarm
    where
        F: 'static + FnMut()
    { .. }

    // make the alarm un-cancelable, returning the id
    pub fn forget(&mut self) -> AlarmId { .. }

    // stop the alarm from continuing to fire
    pub fn cancel(mut self) -> Closure<FnMut()> { .. }
}

// cancels alarm
impl Drop for RecurringAlarm { .. }

// newtype wrapper around the generated alarm name
struct AlarmId { .. }

impl AlarmId {
    // cancel the alarm with this id
    pub fn cancel(&self);
}

Futures API skeletons:

pub struct AlarmFuture { .. }

impl AlarmFuture {
    // Create a new alarm future that will become ready in `millis` ms, once polled
    pub fn new(millis: f64, callback: F) -> Alarm
    where
        F: 'static + FnOnce()
    { .. }
}

// cancels alarm
impl Drop for AlarmFuture { .. }

impl Future for AlarmFuture {
    type Item = ();
    type Error = ();

    ..
}

pub struct AlarmStream { .. }

impl AlarmStream {
    // create a new alarm stream that will fire first in `minutes` minutes, and then again every `minutes` minutes, once polled
    pub fn new<F>(minutes: f64, callback: F) -> AlarmStream
    where
        F: 'static + FnMut()
    { .. }

    // create a new alarm stream that will fire first after `delay` minutes, and then again every `minutes` minutes, once polled
    pub fn new_with_delay<F>(minutes: f64, delay: f64, callback: F) -> AlarmStream
    where
        F: 'static + FnMut()
    { .. }
}

// cancels alarm
impl Drop for AlarmStream { .. }

impl Stream for AlarmStream {
    type Item = ();
    type Error = ();

    ..
}

Prior Art

  • gloo-timers
  • tokio-timer may also be a good source of inspiration.

Drawbacks, Rationale, and Alternatives

One alternative would be to design the API to more closely match the raw API, rather than basing this off of gloo-timers.

Unresolved Questions

Is generating a unique alarm name the right thing to do or should that be the user's responsibility as it is for the raw API?

Web GL

Can we make something like glium for Web GL? Does it make sense to jump straight there, or should we build an incremental Rustier version of the Web GL APIs before trying to make an Opinionated(tm) library?

Questions about versioning Gloo

I'd like to ask two questions related to versioning:

  1. How strict should we be with backwards compatibility in Gloo?
  2. What is the relationship between the umbrella gloo crate's version and each gloo_whatever crate's version?

Note: I'm assuming that whatever the answers, we are following cargo's interpretation of semver.

Backwards Compatibility

On one end of the spectrum is never breaking backwards compatibility. This has the advantage of making upgrades to the latest version of crates easy for users. The disadvantage is that our APIs, once published, are set in stone: we can only add new APIs, can't ever fix flaws in existing APIs.

The Rust std library takes this to an extreme, where backwards compatibility is guaranteed indefinitely. wasm-bindgen is a little looser in that we haven't ruled out breaking version bumps, but we are trying pretty hard to avoid them. We will likely have a single breaking version before moving to 1.0, after which we likely won't do breaking changes anymore.

The other end of the spectrum is fixing flawed APIs whenever we notice them and releasing breaking version bumps immediately. The advantages are that we can fix flaws immediately, and don't have to maintain code that we don't believe meets our design standards. The disadvantages are more difficult upgrades for users, so they are less likely to get the latest and greatest bug fixes and perf improvements.

Versioning the Umbrella Crate and the Individual Crates

Do we want to keep each individual gloo_whatever crate's version locked to the same version as the umbrella gloo crate or not?

Imagine this scenario: we have gloo, gloo_timers, and gloo_events all at version 0.1.2:

gloo @ 0.1.2
  - gloo_timers @ 0.1.2
  - gloo_events @ 0.1.2

Now we fix a bug in gloo_timers that causes us to bump its version from 0.1.2 to 0.1.3. To get the fix in the umbrella gloo crate, we have to bump it to 0.1.3 as well. But do we also bump gloo_events to 0.1.3 or do we leave it at 0.1.2?

gloo @ 0.1.3
  - gloo_timers @ 0.1.3
  - gloo_events @ 0.1.2 or 0.1.3?

The advantages of keeping everything in lock step is largely a lighter maintenance burden for ourselves, the authors/maintainers of Gloo. The disadvantage is unnecessary version bumps for toolkit crates. This disadvantage is magnified by breaking version bumps: in the previous example, if it was a breaking bump to 0.2.0 instead of a compatible bump to 0.1.3, then we could force an unnecessary breaking bump on gloo_events, which would be kind of crappy for anyone using that crate by itself without the rest of Gloo.

The advantages and disadvantages of doing the minimum cascading version bumps are basically the inverse of those for keeping everything in lock step. Upgrades are easier for users (particularly those that are using individual crates, and not the umbrella crate). However, the maintenance burden is larger for Gloo maintainers.

Note that a breaking change in a gloo_whatever crate will always cascade as a breaking change in the umbrella gloo crate.

Web Worker library

  • Spawning web workers
  • Communicating via postMessage

(Note: this is for when we are not using shared memory and multithreading)

Router/history library

Needs some exploratory survey and design work!

  • # fragment?
  • pushState and history API integration?

Mid-Level File API Proposal

Addresses #47 in part by proposing a mid-level API. The possibility for a higher level API is left open.

File API

The mid-level file API aims to implement File I/O on top of the raw JS apis found here: https://w3c.github.io/FileAPI/ which includes listing files, creating files, and reading files.

General Differences

In general this API will mostly differ from the web_sys interface in the following ways:

No js_sys Types

js_sys tends to be fairly "stringly" typed, so where we have known enums, the crate will provide bespoke types.

This is also true for errors which are usually exposed as JsValues. In this crate they will be exposed as well-formed Rust types.

Callbacks

All callbacks will be of the appropriate closure type (e.g., FnOnce).

The API

FileList

JS provides a basic collection type over files (usually from an tag). In general FileList will be an opaque struct that offers a rich interface to the collection through an Iterator implementation.

Blob

Blob is a "file like" interface. The crate would expose this as a Rust interface from which the specific types File and RawData inherit. These types would be roughly 1 to 1 translations of the File and Blob objects just with less "stringly" typed interfaces. For example, RawData would accept a known list of mime-types

FileReader

The FileReader API is a bit wonky and can easily be used in the incorrect way. For instance, if the user calls readAsArrayBuffer or other "read" APIs before the data is loaded, the operation won't return data. The crate will expose an API that more properly models the "read" state machine.

This will be done through a futures API which takes the user through the state machine:

let file_reader = FileReader::new();
file_reader.read_as_text(&blob).map(move |contents| {
  element.set_inner_html(contents);
})

The various "read" methods will take ownership of self so that users can't try to read multiple files at once. These methods return an InProgressRead struct that is a future and also contains an abort method for aborting the upload.

The events "error", "load", and "loadend" are all representable through the future and thus aren't exposed on the file reader. The events "progress", "loadstart", and "abort" are still exposed as methods that take the appropriate Rust closure.

FileReaderSync

This API would mirror the future based API just in a different namespace.

Drawbacks, Rationale, and Alternatives

FileReader exposes a very different API from its JS counterpart. We could do a more 1 to 1 translation of the API and leave the improved API for a higher-level crate. This would make this crate only a very marginal improvement on what is already exposed in js_sys.

Unresolved Questions

Is there an ergonomic way to expose FileReader "progress", "loadstart", and "abort" events as part of the future API?
Is there a better way to model the relationship between Blob and File? Possibilities include an enum and a File type which contains a Blob type.

Create a compelling example for the README

We have a placeholder right now, but we should have something cool!

Ideally it would use multiple gloo crates (probably via gloo::whatever re-exports).

Maybe using timers and events? Suggestions welcome!

How about impl AsyncRead/AsyncWrite for WebSocket?

Motivation

Would allow frontend and backend code to communicate in an abstract manner on an async Stream. It allows code reuse and prevents every application to have to implement this on top of the existing websocket api

Proposed Solution

Add impl AsyncRead/AsyncWrite to websocket

Alternatives

Allow upgrading an Http connection to an AsyncRead/AsyncWrite without websockets. I have no idea what the implementation of this would look like, and if it's even possible without websockets.

That being said, this alternative would be preferable I think. It means we don't need WS on the server either, just an upgraded http connection.

Additional Context

I kind of need this. If need be I don't mind attempting to implement this myself. If this is deemed a good solution, is there any mentoring possible to hint the way?

Make the timers crate follow submodule design

The crates/timers crate should follow the submodule design that we came up with in #27 and are formalizing in #34.

That is:

  • Move the callbacks-based APIs into a callbacks submodule
  • Move the futures-based APIs into a futures module
    • Make the cargo dependency on the futures crate optional
    • Only expose the futures submodule when the futures feature is enabled

[RFC] API for IndexedDB

Summary

This RFC tries to establish an API for IndexedDB

Motivation

gloo doesn't have an API for IndexedDB, a fundamental technology for asynchronous persistent storage.

Detailed Explanation

Based on the discussions that took place in #3 and the now recently closed #59, I propose an "rust-ish" 1x1 implementation of the idb project that has a better alignment with the goals of the gloo team.

Two APIs are going to provided, a more complex and feature-complete IndexedDbFull and a simpler, quicker and easier IndexedDb that builds on top of IndexedDbFull. This way, the user has the option to choose whatever is best for his use-case.

IndexedDbFull and relatables

impl IndexedDbFull {
    pub fn delete_db(&self, name: &str) {}
    pub fn open(&mut self, database_name: &str, version: f64) -> IndexedDbFullVersionManager {}
    pub fn store(&self, name: &str) -> IndexedDbFullStorageManager;
}

impl IndexedDbFullStoreManager {
    pub fn all_keys(&self) {}
    pub fn clear(&self) { }
    pub fn delete(&self, key: &str) {}
    pub fn get(&self, key: &str) {}
    pub fn set(&self, key: &str, value: &str) {}
    pub fn transaction(&self, mode: &str) {}
}

impl IndexedDbFullVersionManager {
    pub fn upgrade(&self, cb: F) {}
    pub fn blocked(&self, cb: F) {}
    pub fn blocking(&self, cb: F) {}
}

let idb = IndexedDbFull::default();
let v = idb.open("foo-db", 1.0);
v.upgrade(|old, new, transaction| ... );
let s = idb.store("foo-store");
s.set("foo-key", "foo-value");

IndexedDb

Has a single hard-coded database, version and store.

// This is just a demonstration. `IndexedDb` and `IndexedDbFullStoreManager`
// are using the same trait definition for its methods.
impl IndexedDb {
    pub fn all_keys(&self) {}
    pub fn clear(&self) {}
    pub fn delete(&self, key: &str) {}
    pub fn get(&self, key: &str) {}
    pub fn set(&self, key: &str, value: &str) {}
}

let idb = IndexedDb::default();
s.set("foo-key", "foo-value");

The two APIs are using callbacks and their Future and Stream interfaces will have a very similar, if not equal, definition.

Drawbacks, Rationale, and Alternatives

The API focuses on simplicity but there are more complex projects like PouchDB and dexie.js.

Unresolved Questions

Should IndexedDbFullStoreManager, IndexedDbFullVersionManager and IndexedDb use the Fluent Interface pattern?

Timers: allow creating detached timers

Summary

Allow creating Timeouts and Intervals without attaching them.

Motivation

I have the following snippet:

    let game = Rc::new(RefCell::new(game));

    let game_ = game.clone();
    let closure_run: Closure<dyn FnMut()> = Closure::wrap(Box::new(move || {
        let mut game = game_.borrow_mut();
        game.run();
    }));

    let game_ = game.clone();
    let closure: Closure<dyn FnMut(_)> = Closure::wrap(Box::new(move |event: KeyboardEvent| {
        if event.key() == "Enter" {
            {
                let mut game = game_.borrow_mut();
                game.handle_input();
            }
            window
                .set_timeout_with_callback(closure_run.as_ref().unchecked_ref())
                .unwrap();
        }
    }));
    input
        .add_event_listener_with_callback("keydown", closure.as_ref().unchecked_ref())
        .unwrap();
    closure.forget();

I tried -- perhaps not hard enough -- to port it to gloo, but it didn't seem to work. Notice that I'm creating closure_run outside of closure. If I switch the closure code to Timeout, it doesn't compile because closure is FnMut, and Timeout::new consumes the callback. And if I try to move the code inside, I get other borrow checker errors.

Notifications API library

Summary

Make an API for displaying notifications (see MDN documentation).

Motivation

The event handlers in web-sys accept a Option<&Function>, which is ugly to work with. Also, it is missing some options.

Detailed Explanation

I propose to use Rust closures instead of Function. Also I'd like a function requesting permission to display notifications, that returns a Future.

Notification::builder() returns a NotificationBuilder, whose build function is called show():

Notification::request_permission()
    .map(|_| {
        let notification = Notification::builder()
            .title("Foo")
            .body("Bar")
            .icon("notification.png")
            .badge("notification-badge.png")
            .image("img.png")
            .silent(true)
            .show();

        on(&notification, move |e: ClickEvent| unimplemented!())
    })

Drawbacks, Rationale, and Alternatives

I don't know of any drawbacks. However, I'm not familiar with other frameworks, so please tell me if there are better alternatives.

Unresolved Questions

The syntax of the event listeners, which is discussed in #43

RAII system for handling event listeners

Summary

I propose to implement an API which makes it easy to use Rust's RAII-based system for event listener registration/deregistration.

Motivation

When registering an event listener, it is often needed to deregister it later.

In JavaScript this is done with the removeEventListener method, but that is verbose and error prone. It also does not clean up Rust resources (e.g. the memory for the closure).

Detailed Explanation

pub struct EventListener<'a, F> where F: ?Sized {
    node: EventTarget,
    kind: &'a str,
    callback: Closure<F>,
}

impl<'a, F> EventListener<'a, F> where F: ?Sized + WasmClosure {
    #[inline]
    pub fn new<F>(node: &EventTarget, kind: &'a str, f: F) -> Self {
        let callback = Closure::wrap(Box::new(f) as Box<F>);

        node.add_event_listener_with_callback(kind, callback.as_ref().unchecked_ref()).unwrap();

        Self {
            node: node.clone(),
            kind,
            callback,
        }
    }
}

impl<'a, F> Drop for EventListener<'a, F> where F: ?Sized {
    #[inline]
    fn drop(&mut self) {
        self.node.remove_event_listener_with_callback(self.kind, self.callback.as_ref().unchecked_ref()).unwrap();
    }
}

The idea is that you can simply call EventListener::new(&node, "foo", move |e| { ... }) and when the EventListener is dropped it will then cleanup the Rust memory for the closure and it will also deregister the event listener.

This is a general purpose idiomatic mid-level API which can work with any event on any DOM node. I expect it to be used pervasively both within Gloo and outside of Gloo.

Drawbacks, Rationale, and Alternatives

For a mid-level API like this, there aren't really any alternatives. Some small things can be tweaked, and I haven't thought very hard about how to support FnOnce (but I'm sure it is possible). Ideas and refinements are welcome.

Unresolved Questions

It is possible to build a higher level API on top of this.

This higher level API gives full static type support for JS events, making it impossible to get the types wrong.

The way that it works is that a new trait like this is created:

pub trait StaticEvent {
    const TAG: &'static str;
    type Event;
    fn wrap(event: Self::Event) -> Self;
}

And a new fn is created, like this:

pub fn on<A, F>(node: &EventTarget, f: F) -> EventListener<'static, FnMut(A::Event)>
    where A: StaticEvent,
          F: FnMut(A) + 'static {
    EventListener::new(node, A::TAG, move |e| {
        f(A::wrap(e));
    })
}

Now you can create various events like this:

struct ClickEvent {
    event: web_sys::MouseEvent,
}

impl ClickEvent {
    // Various methods specific to the `click` event
}

impl StaticEvent for ClickEvent {
    const TAG: &'static str = "click";

    type Event = web_sys::MouseEvent;

    fn wrap(event: Self::Event) -> Self {
        ClickEvent { event }
    }
}
struct MouseDownEvent {
    event: web_sys::MouseEvent,
}

impl MouseDownEvent {
    // Various methods specific to the `mousedown` event
}

impl StaticEvent for MouseDownEvent {
    const TAG: &'static str = "mousedown";

    type Event = web_sys::MouseEvent;

    fn wrap(event: Self::Event) -> Self {
        MouseDownEvent { event }
    }
}

And now finally we can actually use the API:

on(&node, move |e: ClickEvent| {
    ...
})

on(&node, move |e: MouseDownEvent| {
    ...
})

This API has some huge benefits:

  • You no longer need to specify a string for the event type (so no more typos).

  • You no longer need to do any coercions to the correct type (that's all handled automatically now).

  • It guarantees that the static type is correct (in other words, if you have a click event, it is guaranteed to be matched to a ClickEvent Rust type, and vice versa).

This technique was pioneered by stdweb, and it works out really well in practice. It may also be necessary for solving rustwasm/wasm-bindgen#1348

But that's something which can be layered on top of the mid-level EventListener API, so I defer it to later.

WebExtensions crate

Summary

A crate which contains idiomatic high-level Rust APIs for the WebExtensions standard, which is used to create browser extensions for Firefox, Chrome, and Edge.

Motivation

I personally have made multiple Chrome extensions, so this is something I need and have a lot of experience with.

Being able to create fast and safe and maintainable browser extensions in Rust is a big deal. In my experience JavaScript just does not scale well with big extensions.

But the APIs are... well, rather terrible. So in addition to porting the APIs to Rust, we should clean them up and make them actually reasonable.

I've done this sort of work before in JavaScript, so I know a lot of the weird quirks and gotchas of the APIs.

A high-level, convenient, and safe, statically typed event system

Summary

An API that provides guaranteed static typing for events, which is both more convenient and much safer.

Motivation

#30 lays the groundwork for a mid-level event listener system, but it only supports web_sys::Event, and it requires passing in string event types, which are error-prone: typos can happen, and there's no guarantee that the string matches the desired type:

EventListener::new(&foo, "click", move |e| {
    // Oops, "click" isn't a KeyboardEvent! Panic!
    let e: KeyboardEvent = e.dyn_into().unwrap_throw();

})

This proposal completely solves that, by guaranteeing that the static types are always correct.

It also does automatic type casts, which is more convenient for the user (compared to the manual type casts of EventListener).

Detailed Explanation

First, we must create a trait which supports conversion from web_sys::Event:

pub trait StaticEvent<'a> {
    const EVENT_TYPE: &'a str;

    fn unchecked_from_event(event: web_sys::Event) -> Self;
}

Now we create various structs, one struct per event type, and impl StaticEvent for them:

// https://www.w3.org/TR/uievents/#event-type-click
pub struct ClickEvent {
    event: web_sys::MouseEvent,
}

impl ClickEvent {
    // Various methods go here...
}

impl StaticEvent<'static> for ClickEvent {
    const EVENT_TYPE: &'static str = "click";

    #[inline]
    fn unchecked_from_event(event: web_sys::Event) -> Self {
        Self {
            event: event.unchecked_into(),
        }
    }
}

(And similarly for MouseUpEvent, MouseDownEvent, KeyDownEvent, etc.)

If possible, the above should be automatically generated based upon WebIDL.

If that's not possible, we should create a macro that makes it easy to generate impls for StaticEvent:

#[derive(StaticEvent("click"))]
pub struct ClickEvent {
    event: web_sys::MouseEvent,
}

Lastly, there should be an on function which allows for actually using StaticEvent:

pub fn on<'a, A, F>(node: &EventTarget, callback: F) -> EventListener<'a>
    where A: StaticEvent<'a>,
          F: FnMut(A) + 'static {
    EventListener::new(node, A::EVENT_TYPE, move |e| {
        callback(A::unchecked_from_event(e));
    })
}

And now it's possible for users to use the API:

on(&foo, move |e: ClickEvent| {
    // ...
})

on(&foo, move |e: MouseUpEvent| {
    // ...
})

Drawbacks, Rationale, and Alternatives

The primary drawback is the extra complexity, and the extra work of needing to create an extra struct per event type.

However, I don't see any better way to achieve the goals (full static typing, and automatic casts to the correct type).

Unresolved Questions

Perhaps the structs should accept a reference instead? In other words, it would look like this:

pub struct ClickEvent<'a> {
    event: &'a web_sys::MouseEvent,
}

impl<'a> ClickEvent<'a> {
    // Various methods go here...
}

impl<'a> StaticEvent<'a, 'static> for ClickEvent<'a> {
    const EVENT_TYPE: &'static str = "click";

    #[inline]
    fn unchecked_from_event(event: &'a web_sys::Event) -> Self {
        Self {
            event: event.unchecked_ref(),
        }
    }
}

Does that buy us anything?


There is a very important bug which we need to solve: rustwasm/wasm-bindgen#1348

I have given a lot of thought to this, and my (tentative) conclusion is that we can solve this bug by modifying a select few APIs.

In particular, if we change the input, keydown, keypress, and keyup events to do unpaired surrogate checking, that might be good enough to fix everything!

So, in that case, StaticEvent allows us to do that sort of checking:

#[derive(StaticEvent("input"))]
pub struct InputEvent {
    event: web_sys::InputEvent,
}

impl InputEvent {
    // Returns None if there are unpaired surrogates
    fn value(&self) -> Option<String> {
        // Unpaired surrogate checks...
    }
}

And now users can do this:

on(&foo, move |e: InputEvent| {
    if let Some(value) = e.value() {
        // We know there aren't any unpaired surrogates!
    }
})

So this API is not just about convenience or static type safety, it's also about fixing unpaired surrogates!

Parsing framework for javascript objects (serde without the string step)

Summary

Provide a serialization and deserialization framework for converting between javascript values (wasm_bindgen::JsValue) and rust values.

Related proposals:

Motivation

Currently, there are a few different ways to work with javascript values, but they are all sub-optimal.

Case 1: extern type & JsCast

I guess this is the low-level way of working with javascript objects. We don't use reflection like js_sys::Object or js_sys::Reflect. If we use dyn_into, we will get an instanceof check, but there is no check that the values we define in the #[wasm_bindgen] extern block actually match the properties the javascript object contains.

Case 2: js_sys::Object and js_sys::Reflect

These types provide reflection - we can check to see if an object has a property or not. This is a good pragmatic safe way of providing a checked gateway into the typesafe rust land. If we check for all parameters we expect, there is probably some overhead, but we know the values we create in rust are correct and we can avoid panics. TODO I need to understand better what happens if you import a non-existant property in a wasm-bindgen extern block.

Case 3: JsValue::{from/into}_serde

In this case we just convert our javascript type into a string and then re-parse it using the awesome serde framework. This is a reliable way to check we get exactly what we want, but has the following downsides:

  1. It's slow. We have to encode, and then parse needlessly
  2. We can only work with javascript types that work with JSON.stringify. For example, we can't hold javascript functions, and we lose type information converting objects into json.
  3. (related to above) sometimes we want to partially parse a javascript value: for example, we may want to convert an object into rust, but then leave all the properties as opaque js objects. We can't do this here - we have to convert everything.

This proposal aims to provide fine-grained control of parsing exactly what you want to, leaving opaque what you want to, and doing it all without going through a string.

Detailed Explanation

What follows is just my early thoughts on how this might work. It needs iteration.

The serde framework provides a great template for how to generate serialization and deserialization code. I would envisage this working like

#[derive(GlooDeserialize)]
pub struct SomeJsValue {
    // a plain javascript datatype
    my_int: i32,
    // in javascript this would be `value.point == {x: 1, y: 1}`
    // unsure whether to look at Js prototype name or just properties
    point: Subtype,
    // a javascript function that we don't work with - we just pass it back into JS code
    my_func: js_sys::Function,
    // something we don't want to parse, we just check it's there
    arbitrary_value: wasm_bindgen::JsValue,
    // maybe we can distinguish between a property being null, and it not being defined
    #[glooserde(null)]
    should_be_null: (),
}

#[derive(GlooDeserialize)]
pub struct Subtype {
    x: i32,
    y: i32
}

There is some complexity from the fact that we do different things for different types, and special case types from wasm_bindgen/js_sys. For a JsValue, we just have to check the object has a property and then get a handle on the property, for functions, we would check in js that the object is a function, and then for actual objects we use reflection to check the properties are there and then copy them (or a handle for complex objects) into rust.

Drawbacks, Rationale, and Alternatives

The main drawback of this is it's probably a lot of work to implement.

We also already have all the options above, so we could use them, or develop something like gloo-properties. We could possibly build this on top of something like gloo-properties - maybe that would decrease complexity.

Prior art

  • serde
  • how elm parses javascript values - Not sure if this applies directly to this conversation, but anything Evan writes tends to be extremely good. elm goes through JSON like serde.

Unresolved Questions

Basically everything about this. Is it a good idea? How would we implement it? I feel its quite ambitious & complex.

A new Websocket type and crate.

Summary

This is a proposal for a new WebSocket abstraction. One crate with a submodule for a callbacks-based type and another submodule which builds on top of the previous for futures support.

Motivation

WebSockets are fundamental in the web world, and JS land has a plethora of WebSocket libraries with simple APIs. Let's build something on par with these APIs in order to provide the best possible experience.

Without the improvements proposed here:

  • users will have to resort to the callbacks-based usage pattern provided by web_sys::WebSocket type.
  • users may resort to leaking the memory of their WebSocket callbacks which are passed to JS land as there is no current abstraction provided for storing the callbacks.
  • there is some complexity in extracting binary data from a socket frame, with stumbling blocks which could lead to runtime panics.
  • there is currently no support for automatically reconnecting a WebSocket when a connection is lost.

Detailed Explanation

See the original discussion over here in the seed project.

Expand each of the sections below for more details on the two types of WebSocket abstractions being proposed. Everything here has been updated per our discussions in this thread up until this point in time.

callbacks / events based design

websocket with callbacks / events

The plan here is to build on top of the gloo_events crate, which has not yet landed as of 2019.04.04, but which should be landing quite soon.

The essential idea is that we build a new WebSocket type which exposes a builder pattern for creating new instances. This will allow users to easily register their callbacks while building the WebSocket instance. The constructor will then wrap any given callbacks in a gloo_events::EventHandler, register the event handler on the appropriate event coming from the web_sys::WebSocket, and will then retain the event handler for proper cleanup when the WebSocket is dropped.

This abstraction will also enable the futures based WebSocket type (discussed below) to easily build upon this events based WebSocket type.

builder interface example

// Begin building a new WebSocket instance.
// The given `on*` closures will be wrapped by `gloo_events::EventHandler`s.
let ws = WebSocket::connect("/ws")
    // Any protocols which the user would like this connection to use may be specified here.
    .protocols(&["custom.proto.com"])

    // For `onmessage`, users register an FnMut which takes the unpacked
    // data from the received frame. The `WsMessage` enum instance indicates
    // if the message was text or binary. See below for the reasoning behind this.
    .onmessage(|event: WsMessage| ())

    // The other `on*` handlers will be given the raw event object.
    .onopen(|event: Event| ())
    .onerror(|event: Event| ())
    .onclose(|event: Event| ())

    // By default, instances will be configured to reconnect with our `ReconnectConfig::default()`.
    // However, for more control, users may specify customized reconnect config.
    .reconnect(ReconnectConfig)

    // If users want this instance to not perform reconnects at all, this must be called.
    .no_reconnect()

    .build(); // Finalize the build, returning a new WebSocket instance.

/// An enumeration of the different types of WebSocket messages which may be received.
/// 
/// The gloo WebSocket will unpack received messages and present them as the appropriate variant.
pub enum WsMessage {
    Text(String),
    Binary(Vec<u8>),
}

reconnecting

Reconnects will be handled by inserting some logic in the onclose handler (even if a user is has not registered an onclose callback). We will probably just use the Window.set_timeout_with_callback_and_timeout_and_arguments_0 for scheduling the reconnect events based on the backoff algorithm.

We will blow away the old web_sys::WebSocket, build the new one and register the same original EventHandlers. When the new connection opens, we will update the retry state as needed. If an error takes place and the new WebSocket is closed before it ever goes into an open state, we will proceed with the retry algorithm.

We will most likely just use Rc<Cell<_>> or Rc<RefCell<_>> on the web_sys::WebSocket and the reconnect config instance for internally mutating them.

futures based design

websocket with futures

This type will implement the futures Stream + Sink traits which will allow for futures-based reading and writing on the underlying WebSocket, and is built on top of the gloo_events based type described above.

We are also planning to implement AsyncRead + AsyncWrite on this type (per @yoshuawuyts recommendation) which will allow folks to use the tokio_codec::{Encode, Decode} traits for more robust framing on top of the WebSocket.

A set of Rust enums are used for representing the various event types and variants, as all events will come through an internally held futures::sync::mpsc::UnboundedReceiver.

/// An enumeration of all possible event types coming from the underlying WebSocket.
///
/// These are all sent through a futures unbounded channel which the callbacks
/// publish to from within the callbacks-based WebSocket type.
pub enum WSEvent {
    Open(Event),
    Message(WsMessage), // WsMessage is described in the callbacks-based section above.
    Error(Event),
    Close(Event),
}

A few examples of how to build an instance of this futures based type.

// Will build an instance with the default reconnect config.
let ws = WebSocket::connect("/ws");

// Will build an instance which is not configured to reconnect.
let ws = WebSocket::no_reconnect("/ws");

// Will build an instance which uses a custom reconnect config.
let ws = WebSocket::custom_reconnect("/ws", cfg);

// Use sink to send messages & stream to receive messages.
// This comes from https://docs.rs/futures/0.1.25/futures/stream/trait.Stream.html#method.split
let (sink, stream) = ws.split();

// This type also impls AsyncRead + AsyncWrite, so that users can implement
// their own framing protocols (thanks @yoshuawuyts).
use MyEncoderDecoder; // User's custom Encoder + Decoder.
let proto_stream = MyEncoderDecoder::framed(ws)?;
let (mut reader, mut writer) = proto_stream.split(); // Framed reader & writer.

Stream + Sink & AsyncRead + AsyncWrite

Most of this work is actually done. A pull request will be open soon so that we can start looking at an actual implementation.

reconnecting

Reconnecting will actually be handled by the underlying events-based WebSocket instance. Nothing new should need to be implemented here.

Unresolved Questions

  • For the futures based type: what do we want to communicate to users of the AsyncRead + AsyncWrite impls, inasmuch as they would need to treat all data sent and received as binary data (server side included)?
  • This only applies to reconnect-enabled futures-based instances: do we want to buffer outbound frames? If so, for how long? Else, should we just return Ok(AsyncSink::NotReady(msg)) when the underlying WebSocket is being rebuilt?
  • Need to discuss the reconnect config system that we will use. Should we implement our own, or should we use some exponential backoff crate already available?

RxJS-like DOM library

Summary

RxJS is "a library for composing asynchronous and event-based programs by using observable sequences." Its core abstraction is the "observable", which is a collection of future values that has many commonalities with rust streams. I propose to create a similar API for gloo based on streams.

Motivation

gloo needs a library for interacting with DOM events that works well within the idioms of rust. The patterns in RxJS might provide a good fit. Basing it off of streams takes advantage of rust's strong async story, and will only become more ergonomic in the future with features such as async/await and language-level support for streams.

Detailed Explanation

A wrapper type would be created for each element, with a Stream-returning method for each event that the element can emit.

For example, the RxJS smart counter example (which implements a simple odometer effect) might look something like this:

fn smart_counter() -> impl Future<Item = (), Error = ()> {
    let input = gloo_dom::Input::get_by_id("range")
        .expect("expected input element with id #range to exist");
    let update_btn = gloo_dom::Button::get_by_id("update")
        .expect("expected button element with id #update to exist");

    update_btn
        .clicks()
        .map(move |_| input.value().parse::<i32>().unwrap())
        .for_each(move |end| {
            IntervalStream::new(20)
                .zip(stream::iter_ok(0..=end))
                .for_each(|(_, num)| {
                    gloo_dom::Element::get_by_id("display")
                        .expect("expected element with id #display to exist")
                        .set_inner_text(&num.to_string());
                    Ok(())
                })
        })
}

(I prototyped just enough to get this working here)

Drawbacks, Rationale, and Alternatives

Since this would just be another crate in gloo, I think the main alternative is to just not do this.

The major drawback is that this API is not as ergonomic as it could be with rust as it is today, but future language features might change that.

Unresolved Questions

  • Are streams and observables really the same thing? (This has already been discussed here)
  • There's already an RxRust project (that does not use streams IIRC) - does this overlap?

[request a pair of eyes] experiments with history api

I'm playing with wrapping the history api, which should be easy. I've got a crate, but the test in it takes ages to run on firefox, and full-on crashes chrome. It might be a bug in what I've done - but I would appreciate other people taking a look.

The code is currently called wasm-history.

Utility crate for common web_sys features

Summary

I propose adding a crate that wraps common web_sys features with a cleaner API.

Examples:

  • Wrap things like Window, Document and History in a way that handles failures automatically with expect_throw
  • Provide shims to get or set the value of input etc elements
  • Wrap console::log and console::error with a more flexible API

Motivation

The web_sys api can be verbose; high-level crates needn't each provide their own wrappers.

Detailed Explanation

Some common features of web_sys are verbose when used directly. It's likely that many Gloo crates will make use of them - let's settle on a general wrapping API higher-level crates can use. Example

Examples:

/// Convenience function to avoid repeating expect logic.
pub fn window() -> web_sys::Window {
    web_sys::window().expect_throw("Can't find the global Window")
}

/// Convenience function to access the web_sys DOM document.
pub fn document() -> web_sys::Document {
    window().document().expect_throw("Can't find document")
}

/// Convenience function to access the web_sys history.
pub fn history() -> web_sys::History {
    window().history().expect_throw("Can't find history")
}

/// Simplify getting the value of input elements; required due to the need to cast
/// from general nodes/elements to HTML__Elements.
pub fn get_value(target: &web_sys::EventTarget) -> String {
    if let Some(input) = target.dyn_ref::<web_sys::HtmlInputElement>() {
        return input.value();
    }
    if let Some(input) = target.dyn_ref::<web_sys::HtmlTextAreaElement>() {
        return input.value();
    }
    if let Some(input) = target.dyn_ref::<web_sys::HtmlSelectElement>() {
        return input.value();
    }
    "".into()
}

/// Similar to get_value
pub fn set_value(target: &web_sys::EventTarget, value: &str) {
    if let Some(input) = target.dyn_ref::<web_sys::HtmlInputElement>() {
        input.set_value(value);
    }
    if let Some(input) = target.dyn_ref::<web_sys::HtmlTextAreaElement>() {
        input.set_value(value);
    }
    if let Some(input) = target.dyn_ref::<web_sys::HtmlSelectElement>() {
        input.set_value(value);
    }
}

Drawbacks, Rationale, and Alternatives

The original web_sys APIs aren't that clumsy - this might clutter up Gloo without providing much benefit. Perhaps we want to use pattern matching to handle failures of web_sys::Window etc instead of using expect_throw.

It's easy to create a collection of helper functions that are intended to be general, but spread over a wide range of uses, some of which may not be common. It's possible that they may cover a smaller set of use-cases than intended. Thin wrappers in general can be seen as obfuscating the original, well-known API. Why build a new API that causes confusion with the one it wraps, when it's not much better?

Unresolved Questions

It's unclear what the scope of this should be. There are surely grey areas about what's appropriate here, and how cohesive/comprehensive it should be. What causes web_sys::Window etc to fail?

I'm certainly biased by my own use-cases. Do y'all think these are, in-fact useful, general wrappers that would see use in many/most Gloo crates?

I've only scratched the surface. What else should be added?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.