diff --git a/.gitignore b/.gitignore index 8d40cd886..486fd5a19 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,7 @@ !/demos/**/Cargo.lock **/.DS_Store -*.iml \ No newline at end of file +*.iml + +# Code coverage files +*.profraw \ No newline at end of file diff --git a/.tarpaulin.toml b/.tarpaulin.toml index f45206ac0..dd25ceae1 100644 --- a/.tarpaulin.toml +++ b/.tarpaulin.toml @@ -32,6 +32,8 @@ exclude = [ "swimos_form_derive", "swimos_agent_derive", "macro_utilities", + "example_client_2_2", + "example_server_2_2" ] workspace = true avoid-cfg-tarpaulin = true diff --git a/Cargo.toml b/Cargo.toml index 712a0e3f0..cb5fb20a7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,7 @@ members = [ "example_apps/join_value", "example_apps/aggregations", "example_apps/time_series", + "example_apps/devguide/2_2/*", ] exclude = [ diff --git a/example_apps/devguide/2_2/example_client/Cargo.toml b/example_apps/devguide/2_2/example_client/Cargo.toml new file mode 100644 index 000000000..9183041a5 --- /dev/null +++ b/example_apps/devguide/2_2/example_client/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "example_client_2_2" +version = "0.1.0" +edition = "2021" + +[dependencies] +tokio = { workspace = true, features = ["full"] } +swimos_client = { path = "../../../../client/swimos_client" } +swimos_form = { path = "../../../../api/swimos_form" } \ No newline at end of file diff --git a/example_apps/devguide/2_2/example_client/src/main.rs b/example_apps/devguide/2_2/example_client/src/main.rs new file mode 100644 index 000000000..2a8531c2f --- /dev/null +++ b/example_apps/devguide/2_2/example_client/src/main.rs @@ -0,0 +1,54 @@ +use swimos_client::{BasicValueDownlinkLifecycle, DownlinkConfig, RemotePath, SwimClientBuilder}; + +#[tokio::main] +async fn main() { + // Build a Swim Client using the default configuration. + // The `build` method returns a `SwimClient` instance and its internal + // runtime future that is spawned below. + let (client, task) = SwimClientBuilder::default().build().await; + let _client_task = tokio::spawn(task); + let handle = client.handle(); + + // Build a path the downlink. + let state_path = RemotePath::new( + // The host address + "ws://0.0.0.0:8080", + // You can provide any agent URI that matches the pattern + // "/example/:id" + "/example/1", + // This is the URI of the ValueLane in our ExampleAgent + "state", + ); + + let lifecycle = BasicValueDownlinkLifecycle::::default() + // Register an event handler that is invoked when the downlink connects to the agent. + .on_linked_blocking(|| println!("Downlink linked")) + // Register an event handler that is invoked when the downlink synchronises its state. + // with the agent. + .on_synced_blocking(|value| println!("Downlink synced with: {value:?}")) + // Register an event handler that is invoked when the downlink receives an event. + .on_event_blocking(|value| println!("Downlink event: {value:?}")); + + // Build our downlink. + // + // This operation may fail if there is a connection issue. + let state_downlink = handle + .value_downlink::(state_path) + .lifecycle(lifecycle) + .downlink_config(DownlinkConfig::default()) + .open() + .await + .expect("Failed to open downlink"); + + for i in 0..10 { + // Update the lane's state. + state_downlink + .set(i) + .await + .expect("Failed to set downlink state"); + } + + tokio::signal::ctrl_c() + .await + .expect("Failed to listen for ctrl-c."); +} diff --git a/example_apps/devguide/2_2/example_server/Cargo.toml b/example_apps/devguide/2_2/example_server/Cargo.toml new file mode 100644 index 000000000..0ede66706 --- /dev/null +++ b/example_apps/devguide/2_2/example_server/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "example_server_2_2" +version = "0.1.0" +edition = "2021" + +[dependencies] +tokio = { workspace = true, features = ["full"] } +swimos = { path = "../../../../swimos", features = ["server"] } +swimos_form = { path = "../../../../api/swimos_form" } \ No newline at end of file diff --git a/example_apps/devguide/2_2/example_server/src/main.rs b/example_apps/devguide/2_2/example_server/src/main.rs new file mode 100644 index 000000000..15e0f2128 --- /dev/null +++ b/example_apps/devguide/2_2/example_server/src/main.rs @@ -0,0 +1,104 @@ +use swimos::{ + agent::{ + agent_lifecycle::HandlerContext, agent_model::AgentModel, event_handler::EventHandler, + lanes::ValueLane, lifecycle, projections, AgentLaneModel, + }, + route::RoutePattern, + server::{Server, ServerBuilder, ServerHandle}, +}; + +use std::{error::Error, time::Duration}; + +#[derive(AgentLaneModel)] +#[projections] +pub struct ExampleAgent { + state: ValueLane, +} + +#[derive(Clone)] +pub struct ExampleLifecycle; + +#[lifecycle(ExampleAgent)] +impl ExampleLifecycle { + // Handler invoked when the agent starts. + #[on_start] + pub fn on_start( + &self, + context: HandlerContext, + ) -> impl EventHandler { + context.effect(|| println!("Starting agent.")) + } + + // Handler invoked when the agent is about to stop. + #[on_stop] + pub fn on_stop( + &self, + context: HandlerContext, + ) -> impl EventHandler { + context.effect(|| println!("Stopping agent.")) + } + + // Handler invoked after the state of 'lane' has changed. + #[on_event(state)] + pub fn on_event( + &self, + context: HandlerContext, + value: &i32, + ) -> impl EventHandler { + let n = *value; + // EventHandler::effect accepts a FnOnce() + // which runs a side effect. + context.effect(move || { + println!("Setting value to: {}", n); + }) + } +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Create a dynamic route for our agents. + let route = RoutePattern::parse_str("/example/:id")?; + // Create an agent model which contains the factory for creating the agent as well + // as the lifecycle which will be run. + let agent = AgentModel::new(ExampleAgent::default, ExampleLifecycle.into_lifecycle()); + + // Create a server builder. + let server = ServerBuilder::with_plane_name("Plane") + // Bind to port 8080 + .set_bind_addr("127.0.0.1:8080".parse().unwrap()) + // For this guide, ensure agents timeout fairly quickly. + // An agent will timeout after they have received no new updates + // for this configured period of time. + .update_config(|config| { + config.agent_runtime.inactive_timeout = Duration::from_secs(20); + }) + // Register the agent against the route. + .add_route(route, agent) + .build() + // Building the server may fail if many routes are registered and some + // are ambiguous. + .await?; + + // Run the server. A tuple of the server's runtime + // future and a handle to the runtime is returned. + let (task, handle) = server.run(); + // Watch for ctrl+c signals + let shutdown = manage_handle(handle); + + // Join on the server and ctrl+c futures. + let (_, result) = tokio::join!(shutdown, task); + + result?; + println!("Server stopped successfully."); + Ok(()) +} + +// Utility function for awaiting a stop signal in the terminal. +async fn manage_handle(mut handle: ServerHandle) { + tokio::signal::ctrl_c() + .await + .expect("Failed to register interrupt handler."); + + println!("Stopping server."); + handle.stop(); +}