1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
//! Yew worker compilable to wasm32-wasi target. It allows to compile and run POSIX-like
//! applications, having access to random and to emulated file system (memfs).
//! On some operations [wasi workers run faster than wasm-bindgen or stdweb](https:://github.com/dunnock/wabench).
//!
//! It depends on [wasi-worker-cli](https://crates.io/crates/wasi-worker-cli) for deployment.
//!
//! Example usage:
//! ```
//! use wasi_worker_yew::{ThreadedWASI, WASIAgent};
//! use yew::agent::*;
//! use wasi_worker::{FileOptions, ServiceOptions, ServiceWorker};
//!
//! pub struct MyAgent;
//! impl Agent for MyAgent {
//!     type Reach = Public;
//!     type Message = String;
//!     type Input = String;
//!     type Output = String;
//!     fn create(_link: AgentLink<Self>) -> Self { MyAgent { } }
//!     fn update(&mut self, _msg: Self::Message) { /* ... */ }
//!     fn handle(&mut self, _msg: Self::Input, _who: HandlerId) { /* */ }
//!     // link to the JavaScript runner, worker instantiated from:
//!     fn name_of_resource() -> &'static str { "worker.js" }
//! };
//!
//! // In usual WASI setup with JS glue all output will be posted to /output.bin
//! // Though in user filesystem output goes under ./output.bin
//! let opt = ServiceOptions::default().with_cleanup();
//! let output_file = match &opt.output { FileOptions::File(path) => path.clone() };
//! ServiceWorker::initialize(opt)
//!   .expect("ServiceWorker::initialize");
//! ServiceWorker::set_message_handler(Box::new(WASIAgent::<MyAgent>::new()));
//! ```

pub use wasi_worker::{FileOptions, ServiceOptions, ServiceWorker};
pub use yew::agent::{Agent, AgentLink, FromWorker, HandlerId, Packed, Public, ToWorker};

use std::io;
use wasi_worker::Handler;
use yew::agent::{AgentScope, AgentLifecycleEvent, Responder};

/// WASIAgent is the main executor and communication bridge for yew Agent with Reach = Public
pub struct WASIAgent<T: Agent<Reach = Public>> {
    scope: AgentScope<T>,
}

impl<T: Agent<Reach = Public>> WASIAgent<T> {
    pub fn new() -> Self {
        Self {
            scope: AgentScope::<T>::new(),
        }
    }
}

/// Implements rules to register a worker in a separate thread.
pub trait ThreadedWASI {
    /// Creates Agent Scope, initialized AgentLink
    /// It will also create ServiceWorker and return it's instance
    /// ServiceWorker should be used by context to pass messages via on_message
    fn run(&self) -> io::Result<()>;
}

impl<T: Agent<Reach = Public>> ThreadedWASI for WASIAgent<T> {
    fn run(&self) -> io::Result<()> {
        let responder = WASIResponder {};
        let link = AgentLink::connect(&self.scope, responder);
        let upd = AgentLifecycleEvent::Create(link);
        self.scope.send(upd);
        let loaded: FromWorker<T::Output> = FromWorker::WorkerLoaded;
        let loaded = loaded.pack();
        ServiceWorker::post_message(&loaded)
    }
}

impl<T: Agent<Reach = Public>> Handler for WASIAgent<T> {
    fn on_message(&self, data: &[u8]) -> io::Result<()> {
        let msg = ToWorker::<T::Input>::unpack(&data);
        match msg {
            ToWorker::Connected(id) => {
                let upd = AgentLifecycleEvent::Connected(id);
                self.scope.send(upd);
            }
            ToWorker::ProcessInput(id, value) => {
                let upd = AgentLifecycleEvent::Input(value, id);
                self.scope.send(upd);
            }
            ToWorker::Disconnected(id) => {
                let upd = AgentLifecycleEvent::Disconnected(id);
                self.scope.send(upd);
            }
            ToWorker::Destroy => {
                let upd = AgentLifecycleEvent::Destroy;
                self.scope.send(upd);
                std::process::exit(1);
            }
        };
        Ok(())
    }
}

struct WASIResponder {}

// Sending message from worker via ServiceWorker channel
//
// In case of sending message failed it will place error to stderr, which should print to console.
impl<T: Agent<Reach = Public>> Responder<T> for WASIResponder {
    fn respond(&self, id: HandlerId, output: T::Output) {
        let msg = FromWorker::ProcessOutput(id, output);
        let data = msg.pack();
        if let Err(err) = ServiceWorker::post_message(&data) {
            eprintln!("Worker failed to send message: {:?}", err);
        };
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use wasi_worker::{FileOptions, ServiceOptions};

    struct MyAgent;
    impl Agent for MyAgent {
        type Reach = Public;
        type Message = String;
        type Input = String;
        type Output = String;
        fn create(_link: AgentLink<Self>) -> Self {
            MyAgent {}
        }
        fn update(&mut self, _msg: Self::Message) { /* ... */
        }
        fn handle_input(&mut self, _msg: Self::Input, _who: HandlerId) { /* */
        }
    }

    #[test]
    fn it_works() {
        let opt = ServiceOptions {
            output: FileOptions::File("./testdata/output.bin".to_string()),
            cleanup: true,
        };
        ServiceWorker::initialize(opt).expect("ServiceWorker::initialize");
        ServiceWorker::set_message_handler(Box::new(WASIAgent::<MyAgent>::new()));
        let message = b"check";
        ServiceWorker::post_message(message).expect("ServiceWorker::post_message");
        let data = std::fs::read("./testdata/output.bin").expect("Read testdata/output.bin");
        assert_eq!(data, message);
    }
}