Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multi-threading, concurrency, agents #272

Merged
merged 37 commits into from
Jun 16, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
6e0dd80
Add agents
therustmonk May 31, 2018
72a675f
Wrap messages for an agent
therustmonk Jun 2, 2018
8c4f604
Add fetch service to multi thread example
therustmonk Jun 2, 2018
8c3e075
Add an environment for agents
therustmonk Jun 3, 2018
c8515af
Move spawn and register methods to a separate `Worker` trait
therustmonk Jun 3, 2018
1e0e188
Activate agent in the scope
therustmonk Jun 3, 2018
04591f1
Add link to agent's scope
therustmonk Jun 3, 2018
b0df296
Add interval task to multi-thread example
therustmonk Jun 3, 2018
39716ce
Implement responses for agents
therustmonk Jun 3, 2018
febcb65
Remove context from multi-thread example
therustmonk Jun 3, 2018
d40b911
Refactor prelude module for workers
therustmonk Jun 3, 2018
f9d25a6
Separate component link off a context
therustmonk Jun 3, 2018
a8dd243
Add component's link to all examples
therustmonk Jun 3, 2018
bdc061a
Remove Activator alias
therustmonk Jun 3, 2018
e9c64ad
Rename GameOfLife component to Model
therustmonk Jun 3, 2018
23a02fb
Remove Context argument from Component's methods
therustmonk Jun 3, 2018
22f4ffd
Reexport services
therustmonk Jun 3, 2018
e73f025
Remove context argument from examples
therustmonk Jun 3, 2018
80fe42c
Remove Context type parameter from Runner
therustmonk Jun 4, 2018
064dc8e
Remove Env from tests
therustmonk Jun 4, 2018
12bf3fd
Remove CTX everywhere, but scheduler
therustmonk Jun 4, 2018
ae7f90e
Remove CTX from Scheduler
therustmonk Jun 4, 2018
126f847
Remove CTX type of examples
therustmonk Jun 4, 2018
016dfe7
Make scheduler a singleton and use it as a thread_local global instance
therustmonk Jun 4, 2018
258d55f
Rename Message trait to Transferable
therustmonk Jun 4, 2018
0a30abc
Add visibility scopes for agents
therustmonk Jun 5, 2018
abc9408
Add local job and a prototype of actored context
therustmonk Jun 5, 2018
d340aaf
Add a completely new Context based on actors model (agent)
therustmonk Jun 5, 2018
7451005
Implement separate thread worker with a bridge
therustmonk Jun 7, 2018
a6b64b7
Add CHANGELOG.md
therustmonk Jun 8, 2018
edcc898
Update README.md with Agents
therustmonk Jun 8, 2018
41a22eb
Fix tests for virtual component
therustmonk Jun 8, 2018
9784dae
Rename Worker trait to Bridged
therustmonk Jun 8, 2018
8339292
Send Connected and Disconnected messages for Job and Context
therustmonk Jun 9, 2018
bee7756
Detach worker registration from a main initialization routine
therustmonk Jun 9, 2018
4ed0376
Derive Eq, PartialEq and Hash for HandlerId
therustmonk Jun 16, 2018
9441df8
Add information about the new html macro syntax with move modifier
therustmonk Jun 16, 2018
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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