diff --git a/crux_core/src/command/mod.rs b/crux_core/src/command/mod.rs index 7ea8eeafb..c63a52d1f 100644 --- a/crux_core/src/command/mod.rs +++ b/crux_core/src/command/mod.rs @@ -244,14 +244,14 @@ where self.effects.is_empty() && self.events.is_empty() && self.tasks.is_empty() } - /// Run the effect state machine until it settles and collect all effects generated + /// Run the effect state machine until it settles and return an iterator over the effects pub fn effects(&mut self) -> impl Iterator + '_ { self.run_until_settled(); self.effects.try_iter() } - /// Run the effect state machine until it settles and collect all events generated + /// Run the effect state machine until it settles and return an iterator over the events pub fn events(&mut self) -> impl Iterator + '_ { self.run_until_settled(); diff --git a/crux_core/src/testing.rs b/crux_core/src/testing.rs index 4e8096964..0b83bfafb 100644 --- a/crux_core/src/testing.rs +++ b/crux_core/src/testing.rs @@ -7,7 +7,7 @@ use crate::{ channel::Receiver, executor_and_spawner, CommandSpawner, Operation, ProtoContext, QueuingExecutor, }, - Request, WithContext, + Command, Request, WithContext, }; /// AppTester is a simplified execution environment for Crux apps for use in @@ -237,6 +237,25 @@ impl Update { } } +impl Command +where + Effect: Send + 'static, + Event: Send + 'static, +{ + /// Assert that the Command contains _exactly_ one effect and zero events, + /// and return the effect + pub fn expect_one_effect(&mut self) -> Effect { + if self.events().next().is_some() { + panic!("Expected only one effect, but found an event"); + } + if let Some(effect) = self.effects().next() { + effect + } else { + panic!("Expected one effect but found none"); + } + } +} + /// Panics if the pattern doesn't match an `Effect` from the specified `Update` /// /// Like in a `match` expression, the pattern can be optionally followed by `if` diff --git a/crux_platform/src/command.rs b/crux_platform/src/command.rs new file mode 100644 index 000000000..2433d0547 --- /dev/null +++ b/crux_platform/src/command.rs @@ -0,0 +1,20 @@ +use std::{future::Future, marker::PhantomData}; + +use crux_core::{command::RequestBuilder, Command, Request}; + +use crate::{PlatformRequest, PlatformResponse}; + +pub struct Platform { + effect: PhantomData, + event: PhantomData, +} + +impl Platform +where + Effect: From> + Send + 'static, + Event: Send + 'static, +{ + pub fn get() -> RequestBuilder> { + Command::request_from_shell(PlatformRequest) + } +} diff --git a/crux_platform/src/lib.rs b/crux_platform/src/lib.rs index b431f1564..6a2a6e1ee 100644 --- a/crux_platform/src/lib.rs +++ b/crux_platform/src/lib.rs @@ -1,5 +1,7 @@ //! TODO mod docs +pub mod command; + use crux_core::capability::{CapabilityContext, Operation}; use crux_core::macros::Capability; use serde::{Deserialize, Serialize}; diff --git a/examples/cat_facts/Cargo.lock b/examples/cat_facts/Cargo.lock index 2b098b3dc..35a8f2c07 100644 --- a/examples/cat_facts/Cargo.lock +++ b/examples/cat_facts/Cargo.lock @@ -428,9 +428,7 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crux_core" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "583bd1d80ad5366138f6e805876841e314e30709c741e0de24f4d56a532f3b02" +version = "0.11.2" dependencies = [ "anyhow", "bincode", @@ -448,9 +446,7 @@ dependencies = [ [[package]] name = "crux_http" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "438d48d7b0cea78f49ddc53f07aad61562f16ca5ea749d2461503594faf9d328" +version = "0.11.5" dependencies = [ "anyhow", "async-trait", @@ -471,8 +467,6 @@ dependencies = [ [[package]] name = "crux_kv" version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "361568957548828c80ab894c7e384d6b090491f9f690b987c227b34cd31f6855" dependencies = [ "anyhow", "crux_core", @@ -484,8 +478,6 @@ dependencies = [ [[package]] name = "crux_macros" version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "110cb24c2e0842bc5dd2d4dff1bda74a56143d0ce20771cdaabad80ee3567a4f" dependencies = [ "darling", "proc-macro-error", @@ -497,8 +489,6 @@ dependencies = [ [[package]] name = "crux_platform" version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6dd34e3f4d895243b5f81013cac6d0d5e972349d1943d8d86098975ef1633e7" dependencies = [ "crux_core", "serde", @@ -507,11 +497,10 @@ dependencies = [ [[package]] name = "crux_time" version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1cd273d99b8b5a54f589aafdb0bb10d98f8e3875fed7d478b0f3a5f3ef29f8d" dependencies = [ "chrono", "crux_core", + "futures", "serde", "thiserror 2.0.11", ] diff --git a/examples/cat_facts/Cargo.toml b/examples/cat_facts/Cargo.toml index 68fdedf99..5be41d003 100644 --- a/examples/cat_facts/Cargo.toml +++ b/examples/cat_facts/Cargo.toml @@ -12,16 +12,16 @@ rust-version = "1.66" [workspace.dependencies] anyhow = "1.0.95" -# crux_core = { path = "../../crux_core" } -# crux_http = { path = "../../crux_http" } -# crux_kv = { path = "../../crux_kv" } -# crux_platform = { path = "../../crux_platform" } -# crux_time = { path = "../../crux_time", features = ["chrono"] } -crux_core = "0.11.1" -crux_http = "0.11.4" -crux_kv = "0.6.0" -crux_platform = "0.3.0" -crux_time = { version = "0.8.0", features = ["chrono"] } +crux_core = { path = "../../crux_core" } +crux_http = { path = "../../crux_http" } +crux_kv = { path = "../../crux_kv" } +crux_platform = { path = "../../crux_platform" } +crux_time = { path = "../../crux_time", features = ["chrono"] } +# crux_core = "0.11.1" +# crux_http = "0.11.4" +# crux_kv = "0.6.0" +# crux_platform = "0.3.0" +# crux_time = { version = "0.8.0", features = ["chrono"] } serde = "1.0.217" [workspace.metadata.bin] diff --git a/examples/cat_facts/shared/src/app.rs b/examples/cat_facts/shared/src/app.rs index ff8ca809f..122983ff9 100644 --- a/examples/cat_facts/shared/src/app.rs +++ b/examples/cat_facts/shared/src/app.rs @@ -7,13 +7,11 @@ use serde::{Deserialize, Serialize}; pub use crux_core::App; use crux_core::{ render::{self, Render}, - Capability, Command, + Command, }; -use crux_kv::{command::KeyValue as key_value, error::KeyValueError, KeyValue}; +use crux_kv::{command::KeyValue, error::KeyValueError}; use crux_platform::Platform; -use crux_time::{Time, TimeResponse}; - -use platform::Capabilities; +use crux_time::{command::Time, TimeResponse}; const CAT_LOADING_URL: &str = "https://c.tenor.com/qACzaJ1EBVYAAAAd/tenor.gif"; const FACT_API_URL: &str = "https://catfact.ninja/fact"; @@ -90,24 +88,13 @@ pub struct CatFacts { #[cfg_attr(feature = "typegen", derive(crux_core::macros::Export))] #[derive(crux_core::macros::Effect)] +#[allow(unused)] pub struct CatFactCapabilities { - #[allow(unused)] http: crux_http::Http, - #[allow(unused)] - key_value: KeyValue, + key_value: crux_kv::KeyValue, platform: Platform, render: Render, - time: Time, -} - -// Allow easily using Platform as a submodule -impl From<&CatFactCapabilities> for Capabilities { - fn from(incoming: &CatFactCapabilities) -> Self { - Capabilities { - platform: incoming.platform.map_event(super::Event::Platform), - render: incoming.render.map_event(super::Event::Platform), - } - } + time: crux_time::Time, } impl App for CatFacts { @@ -121,25 +108,49 @@ impl App for CatFacts { &self, msg: Event, model: &mut Model, - caps: &CatFactCapabilities, + _caps: &CatFactCapabilities, ) -> Command { + self.update(msg, model) + } + + fn view(&self, model: &Model) -> ViewModel { + let fact = match (&model.cat_fact, &model.time) { + (Some(fact), Some(time)) => format!("Fact from {}: {}", time, fact.format()), + (Some(fact), _) => fact.format(), + _ => "No fact".to_string(), + }; + + let platform = + ::view(&self.platform, &model.platform).platform; + + ViewModel { + platform, + fact, + image: model.cat_image.clone(), + } + } +} + +impl CatFacts { + fn update(&self, msg: Event, model: &mut Model) -> Command { match msg { - Event::GetPlatform => { - self.platform - .update(platform::Event::Get, &mut model.platform, &caps.into()); - Command::done() - } - Event::Platform(msg) => { - self.platform.update(msg, &mut model.platform, &caps.into()); - Command::done() - } + Event::GetPlatform => self + .platform + .update(platform::Event::Get, &mut model.platform) + .map_event(Event::Platform) + .map_effect(Into::into), + Event::Platform(msg) => self + .platform + .update(msg, &mut model.platform) + .map_event(Event::Platform) + .map_effect(Into::into), Event::Clear => { model.cat_fact = None; model.cat_image = None; let bytes = serde_json::to_vec(&model).unwrap(); Command::all([ - key_value::set(KEY, bytes).then_send(|_| Event::None), + KeyValue::set(KEY, bytes).then_send(|_| Event::None), render::render(), ]) } @@ -154,6 +165,7 @@ impl App for CatFacts { model.cat_image = Some(CatImage::default()); Command::all([ + render::render(), Http::get(FACT_API_URL) .expect_json() .build() @@ -162,14 +174,12 @@ impl App for CatFacts { .expect_json() .build() .then_send(Event::SetImage), - render::render(), ]) } Event::SetFact(Ok(mut response)) => { model.cat_fact = Some(response.take_body().unwrap()); - caps.time.now(Event::CurrentTime); - Command::done() + Time::now().then_send(Event::CurrentTime) } Event::SetImage(Ok(mut response)) => { model.cat_image = Some(response.take_body().unwrap()); @@ -177,8 +187,8 @@ impl App for CatFacts { let bytes = serde_json::to_vec(&model).unwrap(); Command::all([ - key_value::set(KEY, bytes).then_send(|_| Event::None), render::render(), + KeyValue::set(KEY, bytes).then_send(|_| Event::None), ]) } Event::SetFact(Err(_)) | Event::SetImage(Err(_)) => { @@ -192,12 +202,12 @@ impl App for CatFacts { let bytes = serde_json::to_vec(&model).unwrap(); Command::all([ - key_value::set(KEY, bytes).then_send(|_| Event::None), render::render(), + KeyValue::set(KEY, bytes).then_send(|_| Event::None), ]) } Event::CurrentTime(_) => panic!("Unexpected time response"), - Event::Restore => key_value::get(KEY).then_send(Event::SetState), + Event::Restore => KeyValue::get(KEY).then_send(Event::SetState), Event::SetState(Ok(Some(value))) => match serde_json::from_slice::(&value) { Ok(m) => { *model = m; @@ -219,28 +229,19 @@ impl App for CatFacts { Event::None => Command::done(), } } +} - fn view(&self, model: &Model) -> ViewModel { - let fact = match (&model.cat_fact, &model.time) { - (Some(fact), Some(time)) => format!("Fact from {}: {}", time, fact.format()), - (Some(fact), _) => fact.format(), - _ => "No fact".to_string(), - }; - - let platform = - ::view(&self.platform, &model.platform).platform; - - ViewModel { - platform, - fact, - image: model.cat_image.clone(), +impl From for Effect { + fn from(effect: platform::Effect) -> Self { + match effect { + platform::Effect::Platform(request) => Effect::Platform(request), + platform::Effect::Render(request) => Effect::Render(request), } } } #[cfg(test)] mod tests { - use crux_core::testing::AppTester; use crux_http::{ protocol::{HttpRequest, HttpResponse, HttpResult}, testing::ResponseBuilder, @@ -252,20 +253,18 @@ mod tests { #[test] fn fetch_results_in_set_fact_and_set_image() { - let app = AppTester::::default(); + let app = CatFacts::default(); let mut model = Model::default(); // send fetch event to app - let (mut http_effects, mut render_effects) = app - .update(Event::Fetch, &mut model) - .take_effects_partitioned_by(Effect::is_http); + let mut fetch_command = app.update(Event::Fetch, &mut model); // receive render effect - render_effects.pop_front().unwrap().expect_render(); + fetch_command.effects().next().unwrap().expect_render(); // receive two HTTP effects, one to fetch the fact and one to fetch the image // we'll handle the fact request first - let request = &mut http_effects.pop_front().unwrap().expect_http(); + let mut request = fetch_command.effects().next().unwrap().expect_http(); assert_eq!(request.operation, HttpRequest::get(FACT_API_URL).build()); let a_fact = CatFact { @@ -274,47 +273,43 @@ mod tests { }; // resolve the request with a simulated response from the web API - let event = app - .resolve( - request, - HttpResult::Ok( - HttpResponse::ok() - .body(r#"{ "fact": "cats are good", "length": 13 }"#) - .build(), - ), - ) - .expect("should resolve successfully") - .expect_one_event(); + request + .resolve(HttpResult::Ok( + HttpResponse::ok() + .body(r#"{ "fact": "cats are good", "length": 13 }"#) + .build(), + )) + .expect("should resolve successfully"); // check that the app emitted an (internal) event to update the model + let event = fetch_command.events().next().unwrap(); assert_eq!( event, Event::SetFact(Ok(ResponseBuilder::ok().body(a_fact.clone()).build())) ); - // Setting the fact should trigger a time event - let mut time_events = app.update(event, &mut model).take_effects(|e| e.is_time()); + // Setting the fact should trigger a time effect + let mut cmd = app.update(event, &mut model); - let request = &mut time_events.pop_front().unwrap().expect_time(); + let request = &mut cmd.effects().next().unwrap().expect_time(); let response = TimeResponse::Now { instant: Instant::new(0, 0).unwrap(), }; - let event = app - .resolve(request, response) - .expect("should resolve successfully") - .expect_one_event(); + request + .resolve(response.clone()) + .expect("should resolve successfully"); + let event = cmd.events().next().unwrap(); assert_eq!(event, Event::CurrentTime(response)); // update the app with the current time event - // and check that we get a key value set event and a render event - let (mut key_value_effects, mut render_effects) = app - .update(event, &mut model) - .take_effects_partitioned_by(Effect::is_key_value); - render_effects.pop_front().unwrap().expect_render(); + // and check that we get a render event ... + let mut cmd = app.update(event, &mut model); + cmd.effects().next().unwrap().expect_render(); - let request = &mut key_value_effects.pop_front().unwrap().expect_key_value(); + // ... and a key value set event + let mut request = cmd.effects().next().unwrap().expect_key_value(); assert_eq!( request.operation, KeyValueOperation::Set { @@ -323,18 +318,16 @@ mod tests { } ); - let _updated = app.resolve_to_event_then_update( - request, - KeyValueResult::Ok { + request + .resolve(KeyValueResult::Ok { response: KeyValueResponse::Set { previous: Value::None, }, - }, - &mut model, - ); + }) + .unwrap(); // Now we'll handle the image - let request = &mut http_effects.pop_front().unwrap().expect_http(); + let mut request = fetch_command.effects().next().unwrap().expect_http(); assert_eq!(request.operation, HttpRequest::get(IMAGE_API_URL).build()); let an_image = CatImage { @@ -342,7 +335,16 @@ mod tests { }; let response = HttpResult::Ok(HttpResponse::ok().body(r#"{"href":"image_url"}"#).build()); - let _updated = app.resolve_to_event_then_update(request, response, &mut model); + request.resolve(response).unwrap(); + + let event = fetch_command.events().next().unwrap(); + assert_eq!( + event, + Event::SetImage(Ok(ResponseBuilder::ok().body(an_image.clone()).build())) + ); + + let mut cmd = app.update(event, &mut model); + cmd.effects().next().unwrap().expect_render(); assert_eq!(model.cat_fact, Some(a_fact)); assert_eq!(model.cat_image, Some(an_image)); diff --git a/examples/cat_facts/shared/src/app/platform.rs b/examples/cat_facts/shared/src/app/platform.rs index a34ed4cd1..dbb5386a2 100644 --- a/examples/cat_facts/shared/src/app/platform.rs +++ b/examples/cat_facts/shared/src/app/platform.rs @@ -2,7 +2,7 @@ use crux_core::{ render::{self, Render}, Command, }; -use crux_platform::{Platform, PlatformResponse}; +use crux_platform::{command::Platform, PlatformResponse}; use serde::{Deserialize, Serialize}; #[derive(Default)] @@ -21,7 +21,7 @@ pub enum Event { #[derive(crux_core::macros::Effect)] pub struct Capabilities { - pub platform: Platform, + pub platform: crux_platform::Platform, pub render: Render, } @@ -32,17 +32,13 @@ impl crux_core::App for App { type Capabilities = Capabilities; type Effect = Effect; - fn update(&self, msg: Event, model: &mut Model, caps: &Capabilities) -> Command { - match msg { - Event::Get => { - caps.platform.get(Event::Set); - Command::done() - } - Event::Set(PlatformResponse(platform)) => { - model.platform = platform; - render::render() - } - } + fn update( + &self, + msg: Event, + model: &mut Model, + _caps: &Capabilities, + ) -> Command { + self.update(msg, model) } fn view(&self, model: &Model) -> Model { @@ -52,27 +48,45 @@ impl crux_core::App for App { } } +impl App { + pub fn update(&self, msg: Event, model: &mut Model) -> Command { + match msg { + Event::Get => Platform::get().then_send(Event::Set), + Event::Set(PlatformResponse(platform)) => { + model.platform = platform; + render::render() + } + } + } +} + #[cfg(test)] mod tests { - use crux_core::testing::AppTester; + use crux_core::App as _; use crux_platform::PlatformResponse; use super::*; #[test] fn get_platform() { - let app = AppTester::::default(); + let app = App::default(); let mut model = Model::default(); - let request = &mut app - .update(Event::Get, &mut model) - .expect_one_effect() - .expect_platform(); + let mut cmd = app.update(Event::Get, &mut model); + let mut request = cmd.expect_one_effect().expect_platform(); + + request + .resolve(PlatformResponse("platform".to_string())) + .unwrap(); + + let set_event = cmd.events().next().unwrap(); + assert_eq!( + set_event, + Event::Set(PlatformResponse("platform".to_string())) + ); - let response = PlatformResponse("platform".to_string()); - app.resolve_to_event_then_update(request, response, &mut model) - .expect_one_effect() - .expect_render(); + let mut cmd = app.update(set_event, &mut model); + cmd.expect_one_effect().expect_render(); assert_eq!(model.platform, "platform"); assert_eq!(app.view(&model).platform, "Hello platform"); diff --git a/examples/cat_facts/web-nextjs/package.json b/examples/cat_facts/web-nextjs/package.json index 77d58139d..ef2d8c901 100644 --- a/examples/cat_facts/web-nextjs/package.json +++ b/examples/cat_facts/web-nextjs/package.json @@ -26,5 +26,6 @@ "@types/react": "19.0.7", "@types/react-dom": "19.0.3", "@types/ua-parser-js": "^0.7.39" - } + }, + "packageManager": "pnpm@9.6.0+sha512.38dc6fba8dba35b39340b9700112c2fe1e12f10b17134715a4aa98ccf7bb035e76fd981cf0bb384dfa98f8d6af5481c2bef2f4266a24bfa20c34eb7147ce0b5e" } diff --git a/examples/notes/Cargo.lock b/examples/notes/Cargo.lock index caed32c65..c87e038ca 100644 --- a/examples/notes/Cargo.lock +++ b/examples/notes/Cargo.lock @@ -105,12 +105,6 @@ dependencies = [ "nom", ] -[[package]] -name = "assert_let_bind" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26da9bc03539eda841c2042fd584ee3982b652606f40dc9e7a828817eae79275" - [[package]] name = "autocfg" version = "1.4.0" @@ -301,9 +295,7 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crux_core" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "583bd1d80ad5366138f6e805876841e314e30709c741e0de24f4d56a532f3b02" +version = "0.11.2" dependencies = [ "anyhow", "bincode", @@ -322,8 +314,6 @@ dependencies = [ [[package]] name = "crux_kv" version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "361568957548828c80ab894c7e384d6b090491f9f690b987c227b34cd31f6855" dependencies = [ "anyhow", "crux_core", @@ -335,8 +325,6 @@ dependencies = [ [[package]] name = "crux_macros" version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "110cb24c2e0842bc5dd2d4dff1bda74a56143d0ce20771cdaabad80ee3567a4f" dependencies = [ "darling", "proc-macro-error", @@ -345,6 +333,16 @@ dependencies = [ "syn 2.0.96", ] +[[package]] +name = "crux_time" +version = "0.8.1" +dependencies = [ + "crux_core", + "futures", + "serde", + "thiserror 2.0.11", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -1015,10 +1013,10 @@ dependencies = [ name = "shared" version = "0.1.0" dependencies = [ - "assert_let_bind", "automerge", "crux_core", "crux_kv", + "crux_time", "futures", "getrandom", "lazy_static", diff --git a/examples/notes/Cargo.toml b/examples/notes/Cargo.toml index 5a1f842f5..ba3a624b3 100644 --- a/examples/notes/Cargo.toml +++ b/examples/notes/Cargo.toml @@ -12,10 +12,11 @@ rust-version = "1.66" [workspace.dependencies] anyhow = "1.0" -# crux_core = { path = "../../crux_core" } -# crux_kv = { path = "../../crux_kv" } -crux_core = "0.11.0" -crux_kv = "0.6.0" +crux_core = { path = "../../crux_core" } +crux_kv = { path = "../../crux_kv" } +crux_time = { path = "../../crux_time" } +# crux_core = "0.11.0" +# crux_kv = "0.6.0" serde = "1.0" [workspace.metadata.bin] diff --git a/examples/notes/shared/Cargo.toml b/examples/notes/shared/Cargo.toml index d34e0b74c..c7e671f2f 100644 --- a/examples/notes/shared/Cargo.toml +++ b/examples/notes/shared/Cargo.toml @@ -21,6 +21,7 @@ name = "shared" automerge = "0.4.1" crux_core.workspace = true crux_kv.workspace = true +crux_time.workspace = true futures = "0.3" lazy_static = "1.5" serde = { workspace = true, features = ["derive"] } @@ -35,6 +36,3 @@ uniffi = { version = "0.28.3", features = ["cli"] } [build-dependencies] uniffi = { version = "0.28.3", features = ["build"] } - -[dev-dependencies] -assert_let_bind = "0.1.1" diff --git a/examples/notes/shared/src/app.rs b/examples/notes/shared/src/app.rs index e5bb76fbf..18ea28556 100644 --- a/examples/notes/shared/src/app.rs +++ b/examples/notes/shared/src/app.rs @@ -7,13 +7,14 @@ use crux_core::{ render::{self, Render}, App, Command, }; -use crux_kv::{error::KeyValueError, KeyValue}; +use crux_kv::{command::KeyValue, error::KeyValueError}; +use crux_time::{ + command::{Time, TimerHandle}, + Duration, TimeResponse, +}; use serde::{Deserialize, Serialize}; -use crate::capabilities::{ - pub_sub::PubSub, - timer::{Timer, TimerOutput}, -}; +use crate::capabilities::pub_sub::PubSub; pub use note::Note; @@ -33,7 +34,7 @@ pub enum Event { Backspace, Delete, ReceiveChanges(Vec), - EditTimer(TimerOutput), + EditTimer(TimeResponse), // events local to the core #[serde(skip)] @@ -54,41 +55,11 @@ impl Default for TextCursor { } } -#[derive(Default)] -struct EditTimer { - current_id: Option, - next_id: u64, -} - -impl EditTimer { - fn start(&mut self, timer: &Timer) { - if let Some(id) = self.current_id { - println!("Cancelling timer {id}"); - timer.cancel(id); - } - self.current_id = None; - - println!("Starting timer {}", self.next_id); - timer.start(self.next_id, EDIT_TIMER, Event::EditTimer); - } - - fn was_created(&mut self, id: u64) { - println!("Timer {id} created, setting next_id to {}", id + 1); - self.next_id = id + 1; - self.current_id = Some(id); - } - - fn finished(&mut self, id: u64) { - println!("Timer {id} finished"); - self.current_id = None; - } -} - #[derive(Default)] pub struct Model { note: Note, cursor: TextCursor, - edit_timer: EditTimer, + timer: Option, } // Same as Model for now, but may change @@ -111,13 +82,13 @@ impl From<&Model> for ViewModel { #[derive(crux_core::macros::Effect)] #[allow(unused)] pub struct Capabilities { - timer: Timer, + timer: crux_time::Time, render: Render, pub_sub: PubSub, - key_value: KeyValue, + key_value: crux_kv::KeyValue, } -const EDIT_TIMER: usize = 1000; +const EDIT_TIMER: u64 = 1000; impl App for NoteEditor { type Event = Event; @@ -131,8 +102,18 @@ impl App for NoteEditor { &self, event: Self::Event, model: &mut Self::Model, - caps: &Self::Capabilities, + _caps: &Self::Capabilities, ) -> Command { + self.update(event, model) + } + + fn view(&self, model: &Self::Model) -> Self::ViewModel { + model.into() + } +} + +impl NoteEditor { + fn update(&self, event: Event, model: &mut Model) -> Command { match event { Event::Insert(text) => { let mut change = match &model.cursor { @@ -144,8 +125,7 @@ impl App for NoteEditor { } }; - caps.pub_sub.publish(change.bytes().to_vec()); - model.edit_timer.start(&caps.timer); + let publish = PubSub::publish(change.bytes().to_vec()); let len = text.chars().count(); let idx = match &model.cursor { @@ -154,28 +134,34 @@ impl App for NoteEditor { }; model.cursor = TextCursor::Position(idx + len); - return render::render(); + Command::all(vec![ + publish, + restart_timer(&mut model.timer), + render::render(), + ]) } Event::Replace(from, to, text) => { let idx = from + text.chars().count(); model.cursor = TextCursor::Position(idx); let mut change = model.note.splice_text(from, to - from, &text); + let publish = PubSub::publish(change.bytes().to_vec()); - caps.pub_sub.publish(change.bytes().to_vec()); - model.edit_timer.start(&caps.timer); - - return render::render(); + Command::all(vec![ + publish, + restart_timer(&mut model.timer), + render::render(), + ]) } Event::MoveCursor(idx) => { model.cursor = TextCursor::Position(idx); - return render::render(); + render::render() } Event::Select(from, to) => { model.cursor = TextCursor::Selection(from..to); - return render::render(); + render::render() } Event::Backspace | Event::Delete => { let (new_index, mut change) = match &model.cursor { @@ -205,11 +191,13 @@ impl App for NoteEditor { }; model.cursor = TextCursor::Position(new_index); + let publish = PubSub::publish(change.bytes().to_vec()); - caps.pub_sub.publish(change.bytes().to_vec()); - model.edit_timer.start(&caps.timer); - - return render::render(); + Command::all(vec![ + publish, + restart_timer(&mut model.timer), + render::render(), + ]) } Event::ReceiveChanges(bytes) => { let change = Change::from_bytes(bytes).expect("a valid change"); @@ -220,44 +208,53 @@ impl App for NoteEditor { model.note.apply_changes_with([change], &mut observer); model.cursor = observer.cursor; - return render::render(); - } - Event::EditTimer(TimerOutput::Created { id }) => { - model.edit_timer.was_created(id); - } - Event::EditTimer(TimerOutput::Finished { id }) => { - model.edit_timer.finished(id); - - caps.key_value - .set("note".to_string(), model.note.save(), Event::Written); + render::render() } + Event::EditTimer(response) => match response { + TimeResponse::DurationElapsed { id: _ } => { + KeyValue::set("note".to_string(), model.note.save()).then_send(Event::Written) + } + _ => Command::done(), + }, Event::Written(_) => { // FIXME assuming successful write + Command::done() } - Event::Open => caps.key_value.get("note".to_string(), Event::Load), + Event::Open => KeyValue::get("note".to_string()).then_send(Event::Load), Event::Load(Ok(value)) => { + let mut commands = Vec::new(); if value.is_none() { model.note = Note::new(); - caps.key_value - .set("note".to_string(), model.note.save(), Event::Written); + commands.push( + KeyValue::set("note".to_string(), model.note.save()) + .then_send(Event::Written), + ); } else { model.note = Note::load(&value.unwrap_or_default()); } - caps.pub_sub.subscribe(Event::ReceiveChanges); - return render::render(); + commands.push(PubSub::subscribe().then_send(Event::ReceiveChanges)); + + commands.push(render::render()); + Command::all(commands) } Event::Load(Err(_)) => { // FIXME handle error + Command::done() } } - - Command::done() } +} - fn view(&self, model: &Self::Model) -> Self::ViewModel { - model.into() +fn restart_timer(current_handle: &mut Option) -> Command { + if let Some(handle) = current_handle.take() { + handle.clear() } + + let duration = Duration::from_millis(EDIT_TIMER).unwrap(); + let (notify_after, handle) = Time::notify_after(duration); + current_handle.replace(handle); + notify_after.then_send(Event::EditTimer) } struct CursorObserver { @@ -309,13 +306,13 @@ impl CursorObserver { #[cfg(test)] mod editing_tests { - use crux_core::{assert_effect, testing::AppTester}; + use crux_core::assert_effect; use super::*; #[test] fn renders_text_and_cursor() { - let app = AppTester::::default(); + let app = NoteEditor::default(); let model = Model { note: Note::with_text("hello"), @@ -334,7 +331,7 @@ mod editing_tests { #[test] fn moves_cursor() { - let app = AppTester::::default(); + let app = NoteEditor::default(); let mut model = Model { note: Note::with_text("hello"), @@ -342,9 +339,8 @@ mod editing_tests { ..Default::default() }; - app.update(Event::MoveCursor(5), &mut model) - .expect_one_effect() - .expect_render(); + let mut cmd = app.update(Event::MoveCursor(5), &mut model); + cmd.expect_one_effect().expect_render(); let view = app.view(&model); @@ -354,7 +350,7 @@ mod editing_tests { #[test] fn changes_selection() { - let app = AppTester::::default(); + let app = NoteEditor::default(); let mut model = Model { note: Note::with_text("hello"), @@ -362,9 +358,8 @@ mod editing_tests { ..Default::default() }; - app.update(Event::Select(2, 5), &mut model) - .expect_one_effect() - .expect_render(); + let mut cmd = app.update(Event::Select(2, 5), &mut model); + cmd.expect_one_effect().expect_render(); let view = app.view(&model); @@ -374,7 +369,7 @@ mod editing_tests { #[test] fn inserts_text_at_cursor_and_renders() { - let app = AppTester::::default(); + let app = NoteEditor::default(); let mut model = Model { note: Note::with_text("hello"), @@ -382,8 +377,8 @@ mod editing_tests { ..Default::default() }; - let update = app.update(Event::Insert("l to the ".to_string()), &mut model); - assert_effect!(update, Effect::Render(_)); + let mut cmd = app.update(Event::Insert("l to the ".to_string()), &mut model); + assert_effect!(cmd, Effect::Render(_)); let view = app.view(&model); @@ -393,7 +388,7 @@ mod editing_tests { #[test] fn replaces_selection_and_renders() { - let app = AppTester::::default(); + let app = NoteEditor::default(); let mut model = Model { note: Note::with_text("hello"), @@ -401,8 +396,8 @@ mod editing_tests { ..Default::default() }; - let update = app.update(Event::Insert("ter skelter".to_string()), &mut model); - assert_effect!(update, Effect::Render(_)); + let mut cmd = app.update(Event::Insert("ter skelter".to_string()), &mut model); + assert_effect!(cmd, Effect::Render(_)); let view = app.view(&model); @@ -412,7 +407,7 @@ mod editing_tests { #[test] fn replaces_range_and_renders() { - let app = AppTester::::default(); + let app = NoteEditor::default(); let mut model = Model { note: Note::with_text("hello"), @@ -420,8 +415,8 @@ mod editing_tests { ..Default::default() }; - let update = app.update(Event::Replace(1, 4, "i, y".to_string()), &mut model); - assert_effect!(update, Effect::Render(_)); + let mut cmd = app.update(Event::Replace(1, 4, "i, y".to_string()), &mut model); + assert_effect!(cmd, Effect::Render(_)); let view = app.view(&model); @@ -431,7 +426,7 @@ mod editing_tests { #[test] fn replaces_empty_range_and_renders() { - let app = AppTester::::default(); + let app = NoteEditor::default(); let mut model = Model { note: Note::with_text("hello"), @@ -439,11 +434,11 @@ mod editing_tests { ..Default::default() }; - let update = app.update( + let mut cmd = app.update( Event::Replace(1, 1, "ey, just saying h".to_string()), &mut model, ); - assert_effect!(update, Effect::Render(_)); + assert_effect!(cmd, Effect::Render(_)); let view = app.view(&model); @@ -453,7 +448,7 @@ mod editing_tests { #[test] fn removes_character_before_cursor() { - let app = AppTester::::default(); + let app = NoteEditor::default(); let mut model = Model { note: Note::with_text("hello"), @@ -461,8 +456,8 @@ mod editing_tests { ..Default::default() }; - let update = app.update(Event::Backspace, &mut model); - assert_effect!(update, Effect::Render(_)); + let mut cmd = app.update(Event::Backspace, &mut model); + assert_effect!(cmd, Effect::Render(_)); let view = app.view(&model); @@ -472,7 +467,7 @@ mod editing_tests { #[test] fn removes_character_after_cursor() { - let app = AppTester::::default(); + let app = NoteEditor::default(); let mut model = Model { note: Note::with_text("hello"), @@ -480,8 +475,8 @@ mod editing_tests { ..Default::default() }; - let update = app.update(Event::Delete, &mut model); - assert_effect!(update, Effect::Render(_)); + let mut cmd = app.update(Event::Delete, &mut model); + assert_effect!(cmd, Effect::Render(_)); let view = app.view(&model); @@ -491,7 +486,7 @@ mod editing_tests { #[test] fn removes_selection_on_delete() { - let app = AppTester::::default(); + let app = NoteEditor::default(); let mut model = Model { note: Note::with_text("hello"), @@ -499,8 +494,8 @@ mod editing_tests { ..Default::default() }; - let update = app.update(Event::Delete, &mut model); - assert_effect!(update, Effect::Render(_)); + let mut cmd = app.update(Event::Delete, &mut model); + assert_effect!(cmd, Effect::Render(_)); let view = app.view(&model); @@ -510,7 +505,7 @@ mod editing_tests { #[test] fn removes_selection_on_backspace() { - let app = AppTester::::default(); + let app = NoteEditor::default(); let mut model = Model { note: Note::with_text("hello"), @@ -518,8 +513,8 @@ mod editing_tests { ..Default::default() }; - let update = app.update(Event::Backspace, &mut model); - assert_effect!(update, Effect::Render(_)); + let mut cmd = app.update(Event::Backspace, &mut model); + assert_effect!(cmd, Effect::Render(_)); let view = app.view(&model); @@ -529,7 +524,7 @@ mod editing_tests { #[test] fn handles_emoji() { - let app = AppTester::::default(); + let app = NoteEditor::default(); let mut model = Model { // the emoji has a skintone modifier, which is a separate unicode character @@ -539,8 +534,8 @@ mod editing_tests { }; // Replace the ' w' after the emoji - let update = app.update(Event::Replace(8, 10, "🥳🙌🏻 w".to_string()), &mut model); - assert_effect!(update, Effect::Render(_)); + let mut cmd = app.update(Event::Replace(8, 10, "🥳🙌🏻 w".to_string()), &mut model); + assert_effect!(cmd, Effect::Render(_)); let view = app.view(&model); @@ -551,17 +546,15 @@ mod editing_tests { #[cfg(test)] mod save_load_tests { - use assert_let_bind::assert_let; - use crux_core::{assert_effect, testing::AppTester}; + use crux_core::assert_effect; use crux_kv::{value::Value, KeyValueOperation, KeyValueResponse, KeyValueResult}; - - use crate::capabilities::timer::{TimerOperation, TimerOutput}; + use crux_time::{TimeRequest, TimerId}; use super::*; #[test] fn opens_a_document() { - let app = AppTester::::default(); + let app = NoteEditor::default(); let mut note = Note::with_text("LOADED"); let mut model = Model { @@ -571,11 +564,10 @@ mod save_load_tests { }; // this will eventually take a document ID - let mut requests = app - .update(Event::Open, &mut model) - .take_effects(Effect::is_key_value); + let mut cmd = app.update(Event::Open, &mut model); + let mut effects = cmd.effects(); - let request = &mut requests.pop_front().unwrap().expect_key_value(); + let request = &mut effects.next().unwrap().expect_key_value(); assert_eq!( request.operation, KeyValueOperation::Get { @@ -583,23 +575,30 @@ mod save_load_tests { } ); - assert!(requests.is_empty()); + assert!(effects.next().is_none()); // Read was successful - let response = KeyValueResult::Ok { - response: KeyValueResponse::Get { - value: note.save().into(), - }, - }; - let update = app.resolve_to_event_then_update(request, response, &mut model); - assert_effect!(update, Effect::Render(_)); + request + .resolve(KeyValueResult::Ok { + response: KeyValueResponse::Get { + value: note.save().into(), + }, + }) + .unwrap(); + drop(effects); + + let load_event = cmd.events().next().unwrap(); + assert!(matches!(load_event, Event::Load(Ok(_)))); + + let mut cmd = app.update(load_event, &mut model); + assert_effect!(cmd, Effect::Render(_)); assert_eq!(app.view(&model).text, "LOADED"); } #[test] fn creates_a_document_if_it_cant_open_one() { - let app = AppTester::::default(); + let app = NoteEditor::default(); let mut model = Model { note: Note::with_text("hello"), @@ -608,11 +607,10 @@ mod save_load_tests { }; // this will eventually take a document ID - let requests = &mut app - .update(Event::Open, &mut model) - .take_effects(Effect::is_key_value); + let mut cmd = app.update(Event::Open, &mut model); + let mut effects = cmd.effects().filter(Effect::is_key_value); - let request = &mut requests.pop_front().unwrap().expect_key_value(); + let request = &mut effects.next().unwrap().expect_key_value(); assert_eq!( request.operation, KeyValueOperation::Get { @@ -620,27 +618,23 @@ mod save_load_tests { } ); - assert!(requests.is_empty()); + assert!(effects.next().is_none()); - // Read was unsuccessful - let event = app - .resolve( - request, - KeyValueResult::Ok { - response: KeyValueResponse::Get { value: Value::None }, - }, - ) - .unwrap() - .expect_one_event(); - - let save = app - .update(event, &mut model) - .into_effects() - .find_map(Effect::into_key_value) + request + .resolve(KeyValueResult::Ok { + response: KeyValueResponse::Get { value: Value::None }, + }) .unwrap(); + drop(effects); + + let load_event = cmd.events().next().unwrap(); + assert!(matches!(load_event, Event::Load(Ok(None)))); + + let mut cmd = app.update(load_event, &mut model); + let request = cmd.effects().find_map(Effect::into_key_value).unwrap(); assert_eq!( - save.operation, + request.operation, KeyValueOperation::Set { key: "note".to_string(), value: model.note.save().into(), @@ -650,7 +644,7 @@ mod save_load_tests { #[test] fn starts_a_timer_after_an_edit() { - let app = AppTester::::default(); + let app = NoteEditor::default(); let mut model = Model { note: Note::with_text("hello"), @@ -659,84 +653,61 @@ mod save_load_tests { }; // An edit should trigger a timer - let requests = &mut app - .update(Event::Insert("something".to_string()), &mut model) - .into_effects() - .filter_map(Effect::into_timer); - - let request = &mut requests.next().unwrap(); - assert_let!( - TimerOperation::Start { - id: first_id, - millis: 1000 - }, - request.operation.clone() - ); + let mut cmd1 = app.update(Event::Insert("something".to_string()), &mut model); + let mut requests = cmd1.effects().filter_map(Effect::into_timer); - assert!(requests.next().is_none()); + let request = requests.next().unwrap(); + let first_id = match &request.operation { + TimeRequest::NotifyAfter { id, duration: _ } => id.clone(), + _ => panic!("expected a NotifyAfter"), + }; - // Tells app the timer was created - let update = app - .resolve(request, TimerOutput::Created { id: first_id }) - .unwrap(); - for event in update.events { - println!("Event: {event:?}"); - let _ = app.update(event, &mut model); - } + assert!(requests.next().is_none()); + drop(requests); // so we can use cmd1 later // Before the timer fires, insert another character, which should // cancel the timer and start a new one - let mut requests = app - .update(Event::Replace(1, 2, "a".to_string()), &mut model) - .into_effects() - .filter_map(Effect::into_timer); - - let cancel_request = requests.next().unwrap(); - assert_let!( - TimerOperation::Cancel { id: cancel_id }, - cancel_request.operation - ); + let mut cmd2 = app.update(Event::Replace(1, 2, "a".to_string()), &mut model); + let mut requests = cmd2.effects().filter_map(Effect::into_timer); + + // but first, the original request (cmd1) should resolve with a clear + let cancel_request = cmd1 + .effects() + .filter_map(Effect::into_timer) + .next() + .unwrap(); + let cancel_id = match &cancel_request.operation { + TimeRequest::Clear { id } => id.clone(), + _ => panic!("expected a Clear"), + }; assert_eq!(cancel_id, first_id); - let start_request = &mut requests.next().unwrap(); - assert_let!( - TimerOperation::Start { - id: second_id, - millis: 1000 - }, - start_request.operation.clone() - ); + // request to start the second timer + let mut start_request = requests.next().unwrap(); + let second_id = match &start_request.operation { + TimeRequest::NotifyAfter { id, duration: _ } => id.clone(), + _ => panic!("expected a NotifyAfter"), + }; + assert_ne!(first_id, second_id); assert!(requests.next().is_none()); - // Tell app the second timer was created - let _updated = app.resolve_to_event_then_update( - start_request, - TimerOutput::Created { id: second_id }, - &mut model, - ); - // Time passes - // Fire the timer - let _updated = app.resolve_to_event_then_update( - start_request, - TimerOutput::Finished { id: second_id }, - &mut model, - ); + start_request + .resolve(TimeResponse::DurationElapsed { id: second_id }) + .unwrap(); // One more edit. Should result in a timer, but not in cancellation - let update = app.update(Event::Backspace, &mut model); - let mut timer_requests = update.into_effects().filter_map(Effect::into_timer); + let mut cmd3 = app.update(Event::Backspace, &mut model); + let mut timer_requests = cmd3.effects().filter_map(Effect::into_timer); - assert_let!( - TimerOperation::Start { - id: third_id, - millis: 1000 - }, - timer_requests.next().unwrap().operation - ); + let start_request = timer_requests.next().unwrap(); + let third_id = match &start_request.operation { + TimeRequest::NotifyAfter { id, duration: _ } => id.clone(), + _ => panic!("expected a NotifyAfter"), + }; assert!(timer_requests.next().is_none()); assert_ne!(third_id, second_id); @@ -744,25 +715,19 @@ mod save_load_tests { #[test] fn saves_document_when_typing_stops() { - let app = AppTester::::default(); + let app = NoteEditor::default(); let mut model = Model { note: Note::with_text("hello"), cursor: TextCursor::Position(5), - edit_timer: EditTimer { - current_id: Some(1), - next_id: 2, - }, + timer: None, }; - let write_request = app - .update( - Event::EditTimer(TimerOutput::Finished { id: 1 }), - &mut model, - ) - .into_effects() - .find_map(Effect::into_key_value) - .unwrap(); + let mut cmd = app.update( + Event::EditTimer(TimeResponse::DurationElapsed { id: TimerId(1) }), + &mut model, + ); + let write_request = cmd.effects().find_map(Effect::into_key_value).unwrap(); assert_eq!( write_request.operation, @@ -778,85 +743,78 @@ mod save_load_tests { mod sync_tests { use std::collections::VecDeque; - use crux_core::{testing::AppTester, Request}; + use crux_core::Request; use crate::capabilities::pub_sub::{Message, PubSubOperation}; use super::*; struct Peer { - app: AppTester, + app: NoteEditor, model: Model, subscription: Option>, + command: Option>, edits: VecDeque>, } // A jig to make testing sync a bit easier - impl Peer { fn new() -> Self { - let app = AppTester::<_>::default(); + let app = NoteEditor::default(); let model = Default::default(); Self { app, model, subscription: None, + command: None, edits: VecDeque::new(), } } // Update, picking out and keeping PubSub effects - fn update(&mut self, event: Event) -> (Vec, Vec) { - let update = self.app.update(event, &mut self.model); - - let mut effects = Vec::new(); - let events = update.events.clone(); + fn update(&mut self, event: Event) { + let mut cmd = self.app.update(event, &mut self.model); - for effect in update.into_effects() { + let mut subscribe = false; + for effect in cmd.effects() { match effect { Effect::PubSub(request) => match request.operation { PubSubOperation::Subscribe => { self.subscription = Some(request); + subscribe = true; } PubSubOperation::Publish(bytes) => { self.edits.push_back(bytes.clone()); } }, - ef => effects.push(ef), + _ => (), } } - (effects, events) + if subscribe { + self.command = Some(cmd); + } } fn view(&self) -> ViewModel { self.app.view(&self.model) } - fn send_edits(&mut self, edits: &[Vec]) -> (Vec, Vec) { - let subscription = self.subscription.as_mut().expect("to have a subscription"); - - let mut effects = Vec::new(); - let mut events = Vec::new(); - - let evs = edits - .iter() - .flat_map(|ed| { - self.app - .resolve(subscription, Message(ed.clone())) - .expect("should resolve") - .events - }) - .collect::>(); - - for event in evs { - let (mut eff, mut ev) = self.update(event); - - effects.append(&mut eff); - events.append(&mut ev); + fn send_edits(&mut self, edits: &[Vec]) { + for edit in edits { + print!("Sending edit: {:?}", edit); + self.subscription + .as_mut() + .expect("to have a subscription") + .resolve(Message(edit.clone())) + .expect("should resolve"); + + if let Some(cmd) = self.command.as_mut() { + for event in cmd.events().collect::>() { + self.update(event.clone()); + } + } } - - (effects, events) } } diff --git a/examples/notes/shared/src/capabilities.rs b/examples/notes/shared/src/capabilities.rs index faa1644b3..db6f3851e 100644 --- a/examples/notes/shared/src/capabilities.rs +++ b/examples/notes/shared/src/capabilities.rs @@ -1,4 +1,3 @@ pub mod pub_sub; -pub mod timer; pub use crux_kv::KeyValueOperation; diff --git a/examples/notes/shared/src/capabilities/pub_sub.rs b/examples/notes/shared/src/capabilities/pub_sub.rs index 0252586f2..15816998a 100644 --- a/examples/notes/shared/src/capabilities/pub_sub.rs +++ b/examples/notes/shared/src/capabilities/pub_sub.rs @@ -1,7 +1,12 @@ -use crux_core::capability::{CapabilityContext, Operation}; -use futures::StreamExt; +use futures::Stream; use serde::{Deserialize, Serialize}; +use crux_core::{ + capability::{CapabilityContext, Operation}, + command::StreamBuilder, + Command, Request, +}; + // TODO add topics #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] @@ -22,40 +27,25 @@ pub struct PubSub { context: CapabilityContext, } -impl PubSub +impl PubSub where - Ev: 'static, + Event: Send + 'static, { - pub fn new(context: CapabilityContext) -> Self { + pub fn new(context: CapabilityContext) -> Self { Self { context } } - pub fn subscribe(&self, make_event: F) + pub fn subscribe() -> StreamBuilder>> where - F: FnOnce(Vec) -> Ev + Clone + Send + 'static, + Effect: From> + Send + 'static, { - self.context.spawn({ - let context = self.context.clone(); - - async move { - let mut stream = context.stream_from_shell(PubSubOperation::Subscribe); - - while let Some(message) = stream.next().await { - let make_event = make_event.clone(); - - context.update_app(make_event(message.0)); - } - } - }) + Command::stream_from_shell(PubSubOperation::Subscribe).map(|Message(data)| data) } - pub fn publish(&self, data: Vec) { - self.context.spawn({ - let context = self.context.clone(); - - async move { - context.notify_shell(PubSubOperation::Publish(data)).await; - } - }) + pub fn publish(data: Vec) -> Command + where + Effect: From> + Send + 'static, + { + Command::notify_shell(PubSubOperation::Publish(data)) } } diff --git a/examples/notes/shared/src/capabilities/timer.rs b/examples/notes/shared/src/capabilities/timer.rs deleted file mode 100644 index c20a3181a..000000000 --- a/examples/notes/shared/src/capabilities/timer.rs +++ /dev/null @@ -1,62 +0,0 @@ -use crux_core::capability::{CapabilityContext, Operation}; -use futures::StreamExt; -use serde::{Deserialize, Serialize}; - -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] -pub enum TimerOperation { - Start { id: u64, millis: usize }, - Cancel { id: u64 }, -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -pub enum TimerOutput { - Created { id: u64 }, - Finished { id: u64 }, -} - -impl Operation for TimerOperation { - type Output = TimerOutput; -} - -#[derive(crux_core::macros::Capability)] -pub struct Timer { - context: CapabilityContext, -} - -impl Timer -where - Ev: 'static, -{ - pub fn new(context: CapabilityContext) -> Self { - Self { context } - } - - pub fn start(&self, id: u64, millis: usize, make_event: F) - where - F: FnOnce(TimerOutput) -> Ev + Clone + Send + 'static, - { - self.context.spawn({ - let context = self.context.clone(); - - async move { - let mut stream = context.stream_from_shell(TimerOperation::Start { id, millis }); - - while let Some(output) = stream.next().await { - let make_event = make_event.clone(); - - context.update_app(make_event(output)); - } - } - }) - } - - pub fn cancel(&self, id: u64) { - self.context.spawn({ - let context = self.context.clone(); - - async move { - context.notify_shell(TimerOperation::Cancel { id }).await; - } - }) - } -} diff --git a/examples/notes/web-nextjs/src/app/core.ts b/examples/notes/web-nextjs/src/app/core.ts index b9230ff8c..18718c57c 100644 --- a/examples/notes/web-nextjs/src/app/core.ts +++ b/examples/notes/web-nextjs/src/app/core.ts @@ -8,7 +8,7 @@ import type { Event, KeyValueResult, Message, - TimerOutput, + TimeResponse, } from "shared_types/types/shared_types"; import { EffectVariantRender, @@ -16,13 +16,9 @@ import { Request, EffectVariantKeyValue, EffectVariantPubSub, - EffectVariantTimer, + EffectVariantTime, PubSubOperationVariantPublish, PubSubOperationVariantSubscribe, - TimerOperationVariantStart, - TimerOutputVariantFinished, - TimerOutputVariantCreated, - TimerOperationVariantCancel, KeyValueOperationVariantGet, KeyValueOperationVariantSet, KeyValueResultVariantOk, @@ -30,6 +26,9 @@ import { KeyValueResponseVariantSet, ValueVariantNone, ValueVariantBytes, + TimeRequestVariantnotifyAfter, + TimeResponseVariantdurationElapsed, + TimeRequestVariantclear, } from "shared_types/types/shared_types"; import { BincodeSerializer, @@ -37,7 +36,7 @@ import { } from "shared_types/bincode/mod"; import { Dispatch, RefObject, SetStateAction } from "react"; -type Response = Message | TimerOutput | KeyValueResult; +type Response = Message | TimeResponse | KeyValueResult; export type Timers = { [key: number]: number; @@ -115,12 +114,14 @@ export class Core { break; } - case EffectVariantTimer: { - const timerOp = (effect as EffectVariantTimer).value; + case EffectVariantTime: { + const timerOp = (effect as EffectVariantTime).value; switch (timerOp.constructor) { - case TimerOperationVariantStart: { - let { id: startId, millis } = timerOp as TimerOperationVariantStart; + case TimeRequestVariantnotifyAfter: { + let { id: startId, duration } = + timerOp as TimeRequestVariantnotifyAfter; + let millis = Number(duration.nanos) / 1e6; let handle = window.setTimeout(() => { // Drop the timer @@ -130,20 +131,18 @@ export class Core { return rest; }); - this.respond(id, new TimerOutputVariantFinished(startId)); - }, Number(millis)); + this.respond(id, new TimeResponseVariantdurationElapsed(startId)); + }, millis); this.setTimers((ts) => ({ [Number(startId)]: handle, ...ts })); - this.respond(id, new TimerOutputVariantCreated(startId)); - break; } - case TimerOperationVariantCancel: { - let { id: cancelId } = timerOp as TimerOperationVariantCancel; + case TimeRequestVariantclear: { + let { id: cancelId } = timerOp as TimeRequestVariantclear; this.setTimers((ts) => { - let { [Number(cancelId)]: handle, ...rest } = ts; + let { [Number(cancelId.value)]: handle, ...rest } = ts; window.clearTimeout(handle); return rest;