Skip to content

Commit

Permalink
Merge #272
Browse files Browse the repository at this point in the history
272: Multi-threading, concurrency, agents r=DenisKolodin a=DenisKolodin

This is a series of bold experiments and I really love 💓 this PR.
It makes this framework a **multi-threaded** (it's not a joke) and brings actors model everywhere.
Now your yew frontend-apps will be more _Erlang_ or _Actix_ apps like 🚀

Also, I've removed a context. Completely! Components simplified. Now it's an actor which you could connect to and interact with messages.
Other benefit is your components could interact each other #270

Since this PR will be merged the framework turned into multi-threaded concurrency-friendly frontend framework. Sorry me for buzzwords overload )

It still need Routing #187 and fixes of the most issues. I'll get to that.
But extra benefit of this PR: it fixes major emscripten issues #220

Remaining:

- [x] Add CHANGELOG.md
- [x] Update README.md
- [x] Create issue: Send `Connected` notification for `Private` agents (#282)
- [x] Create issue: Send `Connected` notification for `Public` agents (#282)
- [x] Create issue: Implement `Global` kind of agents (based on `SharedWorker`) (#283)
- [x] Create issue: Add components interaction example (#284)

Co-authored-by: Denis Kolodin <deniskolodin@gmail.com>
  • Loading branch information
bors[bot] and therustmonk committed Jun 16, 2018
2 parents 7f72c71 + 9441df8 commit b0ff1e0
Show file tree
Hide file tree
Showing 62 changed files with 1,808 additions and 1,060 deletions.
33 changes: 33 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Changelog

## 0.5 - unreleased

### Breaking changes

- Context requirement removed. Not necessary to use `Component<CTX>` type parameter.
Instead of a context a link to an environment provided with `Component::create` call.
All examples had changed.

- `html!` macro adds `move` modifier and the type of event for every handler (#240). Use
`<input oninput=|e| Msg::UpdateEvent(e.value), />` instead of obsolete
`<input oninput=move |e: InputData| Msg::UpdateEvent(e.value), />`.

### New features

- Added `Agent`s concept. Agents are separate activities which you could run in the same thread
or in a separate thread. There is `Context` kind of agent that spawn context entities as many
as you want and you have to interact with a context by a messages. To join an agent use
`Worker::bridge` method and pass a link of component's environment to it.

- Added three types of agents: `Context` - spawns once per thread, `Job` - spawns for every bridge,
`Public` - spawns an agent in a separate thread (it uses [Web Workers API] under the hood).

- Added `<Component: with props />` rule to set a whole struct as a properties of a component.

- All services reexported in `yew::services` moudle.

[Web Workers API]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API

### Bug fixes

- Bug with emscripten target `RuntimeError: index out of bounds` (#220) fixed with a new scheduler.
8 changes: 6 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,12 @@ description = "A framework for making client-side single-page apps"
failure = "0.1"
log = "0.4"
http = "0.1"
serde = "1"
serde_json = "1"
serde = "1.0"
serde_derive = "1.0"
serde_json = "1.0"
bincode = "1.0"
anymap = "0.12"
slab = "0.4"
stdweb = "0.4"
toml = { version = "0.4", optional = true }
serde_yaml = { version = "0.7", optional = true }
Expand Down
142 changes: 116 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@

# Yew

Yew is a modern Rust framework inspired by Elm and ReactJS.
Yew is a modern Rust framework inspired by Elm and ReactJS for
creating multi-threaded frontent apps with WebAssembly.

**NEW!** The framework supports ***multi-threading & concurrency*** out of the box.
It uses [Web Workers API] for spawning actors (agents) in separate threads
and uses a local scheduler attached to a thread for spawning concurrent tasks.

[Web Workers API]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API

[Become a sponsor on Patreon](https://www.patreon.com/deniskolodin)

Expand All @@ -25,25 +32,23 @@ Yew implements strict application state management based on message passing and
extern crate yew;
use yew::prelude::*;

type Context = ();

struct Model { }

enum Msg {
DoIt,
}

impl Component<Context> for Model {
impl Component for Model {
// Some details omitted. Explore the examples to get more.

type Message = Msg;
type Properties = ();

fn create(_: Self::Properties, _: &mut Env<Context, Self>) -> Self {
fn create(_: Self::Properties, _: ComponentLink<Self>) -> Self {
Model { }
}

fn update(&mut self, msg: Self::Message, _: &mut Env<Context, Self>) -> ShouldRender {
fn update(&mut self, msg: Self::Message) -> ShouldRender {
match msg {
Msg::DoIt => {
// Update your model on events
Expand All @@ -53,8 +58,8 @@ impl Component<Context> for Model {
}
}

impl Renderable<Context, Model> for Model {
fn view(&self) -> Html<Context, Self> {
impl Renderable<Model> for Model {
fn view(&self) -> Html<Self> {
html! {
// Render your model here
<button onclick=|_| Msg::DoIt,>{ "Click me!" }</button>
Expand All @@ -64,8 +69,7 @@ impl Renderable<Context, Model> for Model {

fn main() {
yew::initialize();
let app: App<_, Model> = App::new(());
app.mount_to_body();
App::<Model>::new().mount_to_body();
yew::run_loop();
}
```
Expand Down Expand Up @@ -95,6 +99,91 @@ html! {
}
```

### Agents - actors model inspired by Erlang and Actix

Every `Component` could spawn an agent and attach to it.
Agetns are separate tasks which works concurrently.

Create your worker/agent (in `context.rs` for example):

```rust
use yew::prelude::worker::*;

struct Worker {
link: AgentLink<Worker>,
}

#[derive(Serialize, Deserialize, Debug)]
pub enum Request {
Question(String),
}

#[derive(Serialize, Deserialize, Debug)]
pub enum Response {
Answer(String),
}

impl Agent for Worker {
// Available:
// - `Job` (one per bridge)
// - `Context` (shared in the same thread)
// - `Public` (separate thread).
type Reach = Context; // Spawn only one instance per thread (all componentis could reach this)
type Message = Msg;
type Input = Request;
type Output = Response;

// Creates an instance with a link to agent's environment.
fn create(link: AgentLink<Self>) -> Self {
Worker { link }
}

// Implement it for handling inner messages (of services of `send_back` callbacks)
fn update(&mut self, msg: Self::Message) { /* ... */ }

// Implement it for handling incoming messages form components of other agents.
fn handle(&mut self, msg: Self::Input, who: HandlerId) {
match msg {
Request::Question(_) => {
self.link.response(who, Response::Answer("That's cool!".into()));
},
}
}
}
```

Build the bridge to an instance of this agent.
It spawns a worker automatically or reuse an existent (it depends of type of the agent):

```rust
struct Model {
context: Box<Bridge<context::Worker>>,
}

enum Msg {
ContextMsg(context::Response),
}

impl Component for Model {
type Message = Msg;
type Properties = ();

fn create(_: Self::Properties, link: ComponentLink<Self>) -> Self {
let callback = link.send_back(|_| Msg::ContextMsg);
// `Worker::bridge` method spawns an instance if no one available
let context = context::Worker::bridge(callback); // Connected! :tada:
Model { context }
}
}
```

You could use as many agents as you want. For example you could separate all interactions
with a server to a separate thread (real OS thread, because Web Workers maps to native threads).

> **REMEMBER!** Not every APIs available for every environment. For example you couldn't use
`StorageService` from a separate thread that means it won't work with `Public` kind of agent,
but local storage available for `Job` and `Context` kind of agents.

### Components

Yew supports components! You can create a new one by implementing a `Component` trait
Expand Down Expand Up @@ -149,7 +238,7 @@ and supports fine control of rendering.
The `ShouldRender` return value informs the loop when the component should be re-rendered:

```rust
fn update(&mut self, msg: Self::Message, _: &mut Env<Context, Self>) -> ShouldRender {
fn update(&mut self, msg: Self::Message) -> ShouldRender {
match msg {
Msg::UpdateValue(value) => {
self.value = value;
Expand Down Expand Up @@ -190,8 +279,8 @@ You can use external crates and put values from them into the template:
extern crate chrono;
use chrono::prelude::*;

impl Renderable<Context, Model> for Model {
fn view(&self) -> Html<Context, Self> {
impl Renderable<Model> for Model {
fn view(&self) -> Html<Self> {
html! {
<p>{ Local::now() }</p>
}
Expand All @@ -216,23 +305,23 @@ Implemented:
* `WebSocketService`

```rust
use yew::services::console::ConsoleService;
use yew::services::timeout::TimeoutService;
use yew::services::{ConsoleService, TimeoutService};

struct Context {
struct Model {
link: ComponentLink<Model>,
console: ConsoleService,
timeout: TimeoutService<Msg>,
timeout: TimeoutService,
}

impl Component<Context> for Model {
fn update(&mut self, msg: Self::Message, context: &mut Env<Context, Self>) -> ShouldRender {
impl Component for Model {
fn update(&mut self, msg: Self::Message) -> ShouldRender {
match msg {
Msg::Fire => {
let send_msg = context.send_back(|_| Msg::Timeout);
context.timeout.spawn(Duration::from_secs(5), send_msg);
let send_msg = self.link.send_back(|_| Msg::Timeout);
self.timeout.spawn(Duration::from_secs(5), send_msg);
}
Msg::Timeout => {
context.console.log("Timeout!");
self.console.log("Timeout!");
}
}
}
Expand Down Expand Up @@ -288,18 +377,19 @@ struct Client {
}

struct Model {
local_storage: StorageService,
clients: Vec<Client>,
}

impl Component<Context> for Model {
fn update(&mut self, msg: Self::Message, context: &mut Env<Context, Self>) -> ShouldRender {
impl Component for Model {
fn update(&mut self, msg: Self::Message) -> ShouldRender {
Msg::Store => {
// Stores it, but in JSON format/layout
context.local_storage.store(KEY, Json(&model.clients));
self.local_storage.store(KEY, Json(&model.clients));
}
Msg::Restore => {
// Tries to read and destructure it as JSON formatted data
if let Json(Ok(clients)) = context.local_storage.restore(KEY) {
if let Json(Ok(clients)) = self.local_storage.restore(KEY) {
model.clients = clients;
}
}
Expand Down
32 changes: 15 additions & 17 deletions examples/counter/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ extern crate yew;

use stdweb::web::Date;
use yew::prelude::*;
use yew::services::console::ConsoleService;
use yew::services::ConsoleService;

pub struct Model {
console: ConsoleService,
value: i64,
}

Expand All @@ -16,41 +17,38 @@ pub enum Msg {
Bulk(Vec<Msg>),
}

impl<CTX> Component<CTX> for Model
where
CTX: AsMut<ConsoleService>,
{
impl Component for Model {
type Message = Msg;
type Properties = ();

fn create(_: Self::Properties, _: &mut Env<CTX, Self>) -> Self {
Model { value: 0 }
fn create(_: Self::Properties, _: ComponentLink<Self>) -> Self {
Model {
console: ConsoleService::new(),
value: 0,
}
}

fn update(&mut self, msg: Self::Message, env: &mut Env<CTX, Self>) -> ShouldRender {
fn update(&mut self, msg: Self::Message) -> ShouldRender {
match msg {
Msg::Increment => {
self.value = self.value + 1;
env.as_mut().log("plus one");
self.console.log("plus one");
}
Msg::Decrement => {
self.value = self.value - 1;
env.as_mut().log("minus one");
self.console.log("minus one");
}
Msg::Bulk(list) => for msg in list {
self.update(msg, env);
env.as_mut().log("Bulk action");
self.update(msg);
self.console.log("Bulk action");
},
}
true
}
}

impl<CTX> Renderable<CTX, Model> for Model
where
CTX: AsMut<ConsoleService> + 'static,
{
fn view(&self) -> Html<CTX, Self> {
impl Renderable<Model> for Model {
fn view(&self) -> Html<Self> {
html! {
<div>
<nav class="menu",>
Expand Down
17 changes: 1 addition & 16 deletions examples/counter/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,10 @@ extern crate yew;
extern crate counter;

use yew::prelude::*;
use yew::services::console::ConsoleService;
use counter::Model;

pub struct Context {
console: ConsoleService,
}

impl AsMut<ConsoleService> for Context {
fn as_mut(&mut self) -> &mut ConsoleService {
&mut self.console
}
}

fn main() {
yew::initialize();
let context = Context {
console: ConsoleService::new(),
};
let app: App<_, Model> = App::new(context);
app.mount_to_body();
App::<Model>::new().mount_to_body();
yew::run_loop();
}
Loading

0 comments on commit b0ff1e0

Please sign in to comment.