├── .gitignore ├── Cargo.toml ├── README.md ├── README_TEMPLATE.md ├── build.rs ├── changes.md ├── clippy.toml ├── examples └── clicker-game │ ├── .appveyor.yml │ ├── .cargo-ok │ ├── .gitignore │ ├── .travis.yml │ ├── Cargo.toml │ ├── LICENSE_APACHE │ ├── LICENSE_MIT │ ├── README.md │ ├── src │ ├── lib.rs │ └── utils.rs │ ├── tests │ └── web.rs │ └── www │ ├── .bin │ └── create-wasm-app.js │ ├── .gitignore │ ├── .travis.yml │ ├── LICENSE-APACHE │ ├── LICENSE-MIT │ ├── README.md │ ├── bootstrap.js │ ├── index.html │ ├── index.js │ ├── package-lock.json │ ├── package.json │ ├── styles.css │ └── webpack.config.js └── src ├── debug.rs ├── lib.rs ├── nut.rs ├── nut ├── activity.rs ├── activity │ ├── activity_container.rs │ └── lifecycle.rs ├── exec.rs ├── exec │ ├── fifo.rs │ └── inchoate.rs ├── iac.rs └── iac │ ├── filter.rs │ ├── managed_state.rs │ ├── managed_state │ ├── domain_id.rs │ ├── domain_state.rs │ └── domain_store.rs │ ├── publish.rs │ ├── publish │ ├── broadcast.rs │ └── response.rs │ ├── subscription.rs │ └── topic.rs ├── test.rs └── test ├── base_tests.rs ├── domain_tests.rs ├── inchoate_tests.rs └── lifecycle_tests.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | [package] 4 | name = "nuts" 5 | version = "0.2.1" 6 | description = "Nuts is a library that offers a simple publish-subscribe API, featuring decoupled creation of the publisher and the subscriber." 7 | authors = ["Jakob Meier "] 8 | edition = "2018" 9 | license = "MIT/Apache-2.0" 10 | readme = "README.md" 11 | repository = "https://github.com/jakmeier/nuts" 12 | keywords = [ "publish-subscribe", "event", "global-data", "simple-wasm" ] 13 | categories = [ "wasm", "web-programming", "memory-management" ] 14 | 15 | [dependencies] 16 | 17 | # Silence is golden 18 | 19 | # Optional dependency, for debug messages in web console. Only use in debug mode. 20 | web-sys = { optional = true, version = "0.3", features = ['console'] } 21 | 22 | [features] 23 | web-debug = ["web-sys"] 24 | verbose-debug-log = [] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Nuts 3 | 4 | Nuts is a library that offers a simple publish-subscribe API, featuring decoupled creation of the publisher and the subscriber. 5 | 6 | ## Quick first example 7 | ```rust 8 | struct Activity; 9 | let activity = nuts::new_activity(Activity); 10 | activity.subscribe( 11 | |_activity, n: &usize| 12 | println!("Subscriber received {}", n) 13 | ); 14 | nuts::publish(17usize); 15 | // "Subscriber received 17" is printed 16 | nuts::publish(289usize); 17 | // "Subscriber received 289" is printed 18 | ``` 19 | 20 | As you can see in the example above, no explicit channel between publisher and subscriber is necessary. 21 | The call to `publish` is a static method that requires no state from the user. 22 | The connection between them is implicit because both use `usize` as message type. 23 | 24 | Nuts enables this simple API by managing all necessary state in thread-local storage. 25 | This is particularly useful when targeting the web. However, Nuts can be used on other platforms, too. 26 | In fact, Nuts has no dependencies aside from std. 27 | 28 | ## State of Library 29 | With the release of Nuts version 0.2 on [crates.io](https://crates.io/crates/nuts), it has reached an important milestone. 30 | The single-threaded features have all been implemented. Maybe a method here and there needs to be added. But I would not expect to go through major API overhauls again in the existing interface at this point. 31 | 32 | There is one big pending feature left, however. This is parallel dispatch, covered in [#2](https://github.com/jakmeier/nuts/issues/2). 33 | Ideally, that would be implemented under the hood. But likely it will make sense to add some more methods to the API. 34 | 35 | If and when parallel dispatch get implemented, Nuts probably looks at a stable 1.0 release. 36 | 37 | ## Activities 38 | 39 | Activities are at the core of Nuts. 40 | From the globally managed data, they represent the active part, i.e. they can have event listeners. 41 | The passive counter-part is defined by `DomainState`. 42 | 43 | Every struct that has a type with static lifetime (anything that has no lifetime parameter that is determined only at runtime) can be used as an Activity. 44 | You don't have to implement the `Activity` trait yourself, it will always be automatically derived if possible. 45 | 46 | To create an activity, simply register the object that should be used as activity, using `nuts::new_activity` or one of its variants. 47 | 48 | It is important to understand that Activities are uniquely defined by their type. 49 | You cannot create two activities from the same type. (But you can, for example, create a wrapper type around it.) 50 | This allows activities to be referenced by their type, which must be known at run-time. 51 | 52 | ## Publish 53 | 54 | Any instance of a struct or primitive can be published, as long as its type is known at compile-time. (The same constraint as for Activities.) 55 | Upon calling `nuts::publish`, all active subscriptions for the same type are executed and the published object will be shared with all of them. 56 | 57 | ### Example 58 | ```rust 59 | struct ChangeUser { user_name: String } 60 | pub fn main() { 61 | let msg = ChangeUser { user_name: "Donald Duck".to_owned() }; 62 | nuts::publish(msg); 63 | // Subscribers to messages of type `ChangeUser` will be notified 64 | } 65 | ``` 66 | 67 | ## Subscribe 68 | Activities can subscribe to messages, based on the Rust type identifier of the message. Closures or function pointers can be used to create a subscription for a specific type of messages. 69 | 70 | 71 | Nuts uses `core::any::TypeId` internally to compare the types. Subscriptions are called when the type of a published message matches the message type of the subscription. 72 | 73 | There are several different methods for creating new subscriptions. The simplest of them is simply called `subscribe(...)` and it can be used like this: 74 | 75 | 76 | ```rust 77 | struct MyActivity { id: usize }; 78 | struct MyMessage { text: String }; 79 | 80 | pub fn main() { 81 | let activity = nuts::new_activity(MyActivity { id: 0 } ); 82 | activity.subscribe( 83 | |activity: &mut MyActivity, message: &MyMessage| 84 | println!("Subscriber with ID {} received text: {}", activity.id, message.text) 85 | ); 86 | } 87 | ``` 88 | In the example above, a subscription is created that waits for messages of type `MyMessage` to be published. 89 | So far, the code inside the closure is not executed and nothing is printed to the console. 90 | 91 | Note that the first argument of the closure is a mutable reference to the activity object. 92 | The second argument is a read-only reference to the published message. 93 | Both types must match exactly or otherwise the closure will not be accepted by the compiler. 94 | 95 | A function with the correct argument types can also be used to subscribe. 96 | ```rust 97 | struct MyActivity { id: usize }; 98 | struct MyMessage { text: String }; 99 | 100 | pub fn main() { 101 | let activity = nuts::new_activity(MyActivity { id: 0 } ); 102 | activity.subscribe(MyActivity::print_text); 103 | } 104 | 105 | impl MyActivity { 106 | fn print_text(&mut self, message: &MyMessage) { 107 | println!("Subscriber with ID {} received text: {}", self.id, message.text) 108 | } 109 | } 110 | ``` 111 | 112 | ## Example: Basic Activity with Publish + Subscribe 113 | 114 | ```rust 115 | #[derive(Default)] 116 | struct MyActivity { 117 | round: usize 118 | } 119 | struct MyMessage { 120 | no: usize 121 | } 122 | 123 | // Create activity 124 | let activity = MyActivity::default(); 125 | // Activity moves into globally managed state, ID to handle it is returned 126 | let activity_id = nuts::new_activity(activity); 127 | 128 | // Add event listener that listens to published `MyMessage` types 129 | activity_id.subscribe( 130 | |my_activity, msg: &MyMessage| { 131 | println!("Round: {}, Message No: {}", my_activity.round, msg.no); 132 | my_activity.round += 1; 133 | } 134 | ); 135 | 136 | // prints "Round: 0, Message No: 1" 137 | nuts::publish( MyMessage { no: 1 } ); 138 | // prints "Round: 1, Message No: 2" 139 | nuts::publish( MyMessage { no: 2 } ); 140 | ``` 141 | 142 | ## Example: Private Channels 143 | In what I have shown you so far, all messages have been shared reference and it is sent to all listeners that registered to a specific message type. 144 | An alternative is to use private channels. A sender can then decide which listening activity will receive the message. 145 | In that case, the ownership of the message is given to the listener. 146 | 147 | ```rust 148 | struct ExampleActivity {} 149 | let id = nuts::new_activity(ExampleActivity {}); 150 | // `private_channel` works similar to `subscribe` but it owns the message. 151 | id.private_channel(|_activity, msg: usize| { 152 | assert_eq!(msg, 7); 153 | }); 154 | // `send_to` must be used instead of `publish` when using private channels. 155 | // Which activity receives the message is decide by the first type parameter. 156 | nuts::send_to::(7usize); 157 | ``` 158 | 159 | ## Activity Lifecycle 160 | 161 | Each activity has a lifecycle status that can be changed using [`set_status`](https://docs.rs/nuts/0.2.1/nuts/struct.ActivityId.html#method.set_status). 162 | It starts with `LifecycleStatus::Active`. 163 | In the current version of Nuts, the only other status is `LifecycleStatus::Inactive`. 164 | 165 | The inactive status can be used to put activities to sleep temporarily. 166 | While inactive, the activity will not be notified of events it has subscribed to. 167 | A subscription filter can been used to change this behavior. 168 | (See [`subscribe_masked`](https://docs.rs/nuts/0.2.1/nuts/struct.ActivityId.html#method.subscribe_masked)) 169 | 170 | If the status of a changes from active to inactive, the activity's [`on_leave`](https://docs.rs/nuts/0.2.1/nuts/struct.ActivityId.html#method.on_leave) and [`on_leave_domained`](https://docs.rs/nuts/0.2.1/nuts/struct.ActivityId.html#method.on_leave_domained) subscriptions will be called. 171 | 172 | If the status of a changes from inactive to active, the activity's [`on_enter`](https://docs.rs/nuts/0.2.1/nuts/struct.ActivityId.html#method.on_enter) and [`on_enter_domained`](https://docs.rs/nuts/0.2.1/nuts/struct.ActivityId.html#method.on_enter_domained) subscriptions will be called. 173 | 174 | 175 | ## Domains 176 | 177 | 178 | A Domain stores arbitrary data for sharing between multiple [Activities](https://docs.rs/nuts/0.2.1/nuts/trait.Activity.html). 179 | Library users can define the number of domains but each activity can only join one domain. 180 | 181 | Domains should only be used when data needs to be shared between multiple activities of the same or different types. 182 | If data is only used by a single activity, it is usually better to store it in the activity struct itself. 183 | 184 | In case only one domain is used, you can also consider to use [`DefaultDomain`](https://docs.rs/nuts/0.2.1/nuts/struct.DefaultDomain.html) instead of creating your own enum. 185 | 186 | For now, there is no real benefit from using multiple Domains, other than data isolation. 187 | But there are plans for the future that will schedule Activities in different threads, based on their domain. 188 | 189 | ### Creating Domains 190 | Nuts creates domains implicitly in the background. The user can simply provide an enum or struct that implements the `DomainEnumeration` trait. This trait requires only the `fn id(&self) -> usize` function, which maps every object to a number representing the domain. 191 | 192 | Typically, domains are defined by an enum and the `DomainEnumeration` trait is derived using using [`domain_enum!`](macro.domain_enum.html). 193 | 194 | 195 | ``` 196 | #[macro_use] extern crate nuts; 197 | use nuts::{domain_enum, DomainEnumeration}; 198 | #[derive(Clone, Copy)] 199 | enum MyDomain { 200 | DomainA, 201 | DomainB, 202 | } 203 | domain_enum!(MyDomain); 204 | ``` 205 | 206 | ### Using Domains 207 | The function [`nuts::store_to_domain`](fn.store_to_domain.html) allows to initialize data in a domain. Afterwards, the data is available in subscription functions of the activities. 208 | 209 | 210 | Only one instance per type id can be stored inside a domain. 211 | If an old value of the same type already exists in the domain, it will be overwritten. 212 | 213 | If activities are associated with a domain, they must be registered using the [`nuts::new_domained_activity`](fn.new_domained_activity.html). 214 | This will allow to subscribe with closures that have access to domain state. 215 | [`subscribe_domained`](struct.ActivityId.html#method.subscribe_domained) is used to add those subscriptions. 216 | [`subscribe`](struct.ActivityId.html#method.subscribe) can still be used for subscriptions that do not access the domain. 217 | 218 | #### Example of Activity with Domain 219 | 220 | ```rust 221 | use nuts::{domain_enum, DomainEnumeration}; 222 | 223 | #[derive(Default)] 224 | struct MyActivity; 225 | struct MyMessage; 226 | 227 | #[derive(Clone, Copy)] 228 | enum MyDomain { 229 | DomainA, 230 | DomainB, 231 | } 232 | domain_enum!(MyDomain); 233 | 234 | // Add data to domain 235 | nuts::store_to_domain(&MyDomain::DomainA, 42usize); 236 | 237 | // Register activity 238 | let activity_id = nuts::new_domained_activity(MyActivity, &MyDomain::DomainA); 239 | 240 | // Add event listener that listens to published `MyMessage` types and has also access to the domain data 241 | activity_id.subscribe_domained( 242 | |_my_activity, domain, msg: &MyMessage| { 243 | // borrow data from the domain 244 | let data = domain.try_get::(); 245 | assert_eq!(*data.unwrap(), 42); 246 | } 247 | ); 248 | 249 | // make sure the subscription closure is called 250 | nuts::publish( MyMessage ); 251 | ``` 252 | 253 | ## Advanced: Understanding the Execution Order 254 | 255 | When calling `nuts::publish(...)`, the message may not always be published immediately. While executing a subscription handler from previous `publish`, all new messages are queued up until the previous one is completed. 256 | ```rust 257 | struct MyActivity; 258 | let activity = nuts::new_activity(MyActivity); 259 | activity.subscribe( 260 | |_, msg: &usize| { 261 | println!("Start of {}", msg); 262 | if *msg < 3 { 263 | nuts::publish( msg + 1 ); 264 | } 265 | println!("End of {}", msg); 266 | } 267 | ); 268 | 269 | nuts::publish(0usize); 270 | // Output: 271 | // Start of 0 272 | // End of 0 273 | // Start of 1 274 | // End of 1 275 | // Start of 2 276 | // End of 2 277 | // Start of 3 278 | // End of 3 279 | ``` 280 | 281 | ## Full Demo Examples 282 | A simple example using nuts to build a basic clicker game is available in [examples/clicker-game](tree/master/examples/clicker-game). It requires `wasm-pack` installed and `npm`. To run the example, execute `wasm-pack build` and then `cd www; npm install; npm run start`. 283 | This example only shows minimal features of nuts. 284 | 285 | Right now, there are no more examples (some had to be removed due to outdated dependencies). Hopefully that will change at some point. 286 | 287 | All examples are set up as their own project. (To avoid polluting the libraries dependencies.) 288 | Therefore the standard `cargo run --example` will not work. One has to go to the example's directory and build it from there. 289 | -------------------------------------------------------------------------------- /README_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Nuts 2 | @DOC CRATE 3 | 4 | ## State of Library 5 | With the release of Nuts version 0.2 on [crates.io](https://crates.io/crates/nuts), it has reached an important milestone. 6 | The single-threaded features have all been implemented. Maybe a method here and there needs to be added. But I would not expect to go through major API overhauls again in the existing interface at this point. 7 | 8 | There is one big pending feature left, however. This is parallel dispatch, covered in [#2](https://github.com/jakmeier/nuts/issues/2). 9 | Ideally, that would be implemented under the hood. But likely it will make sense to add some more methods to the API. 10 | 11 | If and when parallel dispatch get implemented, Nuts probably looks at a stable 1.0 release. 12 | 13 | ## Activities 14 | @DOC ACTIVITY 15 | 16 | ## Publish 17 | @DOC PUBLISH 18 | 19 | ## Subscribe 20 | Activities can subscribe to messages, based on the Rust type identifier of the message. Closures or function pointers can be used to create a subscription for a specific type of messages. 21 | 22 | 23 | Nuts uses `core::any::TypeId` internally to compare the types. Subscriptions are called when the type of a published message matches the message type of the subscription. 24 | 25 | There are several different methods for creating new subscriptions. The simplest of them is simply called `subscribe(...)` and it can be used like this: 26 | 27 | @DOC SUBSCRIBE_EXAMPLE 28 | 29 | ## Example: Basic Activity with Publish + Subscribe 30 | @DOC NEW_ACTIVITY 31 | 32 | ## Example: Private Channels 33 | In what I have shown you so far, all messages have been shared reference and it is sent to all listeners that registered to a specific message type. 34 | An alternative is to use private channels. A sender can then decide which listening activity will receive the message. 35 | In that case, the ownership of the message is given to the listener. 36 | @DOC PUBLISH_PRIVATE 37 | 38 | ## Activity Lifecycle 39 | @DOC ACTIVITY_LIFECYCLE 40 | 41 | ## Domains 42 | 43 | @DOC DOMAIN 44 | 45 | ### Creating Domains 46 | Nuts creates domains implicitly in the background. The user can simply provide an enum or struct that implements the `DomainEnumeration` trait. This trait requires only the `fn id(&self) -> usize` function, which maps every object to a number representing the domain. 47 | 48 | Typically, domains are defined by an enum and the `DomainEnumeration` trait is derived using using [`domain_enum!`](macro.domain_enum.html). 49 | 50 | @DOC DOMAIN_MACRO_EXAMPLE 51 | 52 | ### Using Domains 53 | The function [`nuts::store_to_domain`](fn.store_to_domain.html) allows to initialize data in a domain. Afterwards, the data is available in subscription functions of the activities. 54 | 55 | @DOC DOMAIN_STORE 56 | 57 | If activities are associated with a domain, they must be registered using the [`nuts::new_domained_activity`](fn.new_domained_activity.html). 58 | This will allow to subscribe with closures that have access to domain state. 59 | [`subscribe_domained`](struct.ActivityId.html#method.subscribe_domained) is used to add those subscriptions. 60 | [`subscribe`](struct.ActivityId.html#method.subscribe) can still be used for subscriptions that do not access the domain. 61 | 62 | #### Example of Activity with Domain 63 | @DOC NEW_ACTIVITY_WITH_DOMAIN 64 | 65 | ## Advanced: Understanding the Execution Order 66 | @DOC PUBLISH_ADVANCED 67 | 68 | ## Full Demo Examples 69 | A simple example using nuts to build a basic clicker game is available in [examples/clicker-game](tree/master/examples/clicker-game). It requires `wasm-pack` installed and `npm`. To run the example, execute `wasm-pack build` and then `cd www; npm install; npm run start`. 70 | This example only shows minimal features of nuts. 71 | 72 | Right now, there are no more examples (some had to be removed due to outdated dependencies). Hopefully that will change at some point. 73 | 74 | All examples are set up as their own project. (To avoid polluting the libraries dependencies.) 75 | Therefore the standard `cargo run --example` will not work. One has to go to the example's directory and build it from there. 76 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::fs; 3 | use std::io::{BufRead, BufReader, BufWriter, Write}; 4 | use std::path::Path; 5 | 6 | const HEADER: &'static str = ""; 7 | 8 | fn main() -> std::io::Result<()> { 9 | if let Ok(_) = std::env::var("DOCS_RS") { 10 | return Ok(()); 11 | } 12 | println!("cargo:rerun-if-changed=build.rs"); 13 | println!("cargo:rerun-if-changed=README_TEMPLATE.md"); 14 | if let Err(_) = std::env::var("REBUILD_README") { 15 | return Ok(()); 16 | } 17 | let out = fs::File::create("README.md")?; 18 | let mut readme = BufWriter::new(out); 19 | 20 | writeln!(readme, "{}", HEADER)?; 21 | 22 | let template = fs::File::open("README_TEMPLATE.md")?; 23 | let template = BufReader::new(template); 24 | 25 | let cargo_version = env!("CARGO_PKG_VERSION"); 26 | let docs_url = format!("https://docs.rs/nuts/{}/nuts/", cargo_version); 27 | let info = ReadmeInfo { docs_url }; 28 | 29 | let mut dict = HashMap::new(); 30 | find_snippets_in_directory("src", &mut dict)?; 31 | for line in template.lines() { 32 | let line = line?; 33 | if line.starts_with("@DOC ") { 34 | let (_, key) = line.split_at(5); 35 | if let Some(doc) = dict.get(key) { 36 | writeln!(readme, "{}", readme_transformation(&doc, &info))?; 37 | } else { 38 | writeln!(readme, "MISSING DOCS: {} not found", key)?; 39 | } 40 | } else { 41 | writeln!(readme, "{}", line)?; 42 | } 43 | } 44 | println!("dictioniary: {:?}", dict); 45 | Ok(()) 46 | } 47 | 48 | #[derive(Debug)] 49 | struct Snippet { 50 | raw: String, 51 | file_path: Vec, 52 | } 53 | 54 | fn find_snippets_in_directory( 55 | dir: impl AsRef, 56 | dict: &mut HashMap, 57 | ) -> std::io::Result<()> { 58 | for entry in fs::read_dir(dir)? { 59 | let entry = entry?; 60 | let path = entry.path(); 61 | 62 | let metadata = fs::metadata(&path)?; 63 | if metadata.is_file() && path.extension().map(|ext| ext == "rs").unwrap_or(false) { 64 | find_snippets_in_file(path, dict)?; 65 | } else if metadata.is_dir() { 66 | find_snippets_in_directory(path, dict)?; 67 | } 68 | } 69 | Ok(()) 70 | } 71 | 72 | #[derive(PartialEq, Debug)] 73 | enum SearchState { 74 | OutsideSnippet, 75 | InsideSnippet, 76 | } 77 | fn find_snippets_in_file( 78 | path: impl AsRef, 79 | dict: &mut HashMap, 80 | ) -> std::io::Result<()> { 81 | let file = fs::File::open(&path)?; 82 | let mut state = SearchState::OutsideSnippet; 83 | let mut key = None; 84 | let mut snippet = String::new(); 85 | const START_MARKER: &'static str = "// @ START-DOC "; 86 | const END_MARKER: &'static str = "// @ END-DOC"; 87 | for (line_number, line) in BufReader::new(file).lines().enumerate() { 88 | let line = line?; 89 | let line = line.trim(); 90 | match state { 91 | SearchState::OutsideSnippet => { 92 | if line.starts_with(START_MARKER) { 93 | state = SearchState::InsideSnippet; 94 | let (_start, end) = line.split_at(START_MARKER.len()); 95 | key = Some(end.to_owned()); 96 | } 97 | } 98 | SearchState::InsideSnippet => { 99 | if line.starts_with(END_MARKER) { 100 | state = SearchState::OutsideSnippet; 101 | dict.insert( 102 | key.take().expect("No key?"), 103 | Snippet { 104 | raw: std::mem::take(&mut snippet), 105 | file_path: path 106 | .as_ref() 107 | .iter() 108 | .map(|s| s.to_str().unwrap().to_owned()) 109 | .collect(), 110 | }, 111 | ); 112 | println!("cargo:rerun-if-changed={}", path.as_ref().to_str().unwrap()); 113 | } else { 114 | snippet += "\n"; 115 | let uncommented = if let Some(i) = line.find("/// ") { 116 | line.split_at(i + 4).1 117 | } else if let Some(i) = line.find("///") { 118 | line.split_at(i + 3).1 119 | } else if let Some(i) = line.find("//! ") { 120 | line.split_at(i + 4).1 121 | } else if let Some(i) = line.find("//!") { 122 | line.split_at(i + 3).1 123 | } else { 124 | panic!( 125 | "Doc comment for README cannot be parsed: file: {}:{}, doc comment: {:?}", 126 | path.as_ref().to_str().unwrap(), 127 | line_number+1, 128 | line 129 | ) 130 | }; 131 | snippet += uncommented; 132 | } 133 | } 134 | } 135 | } 136 | assert_eq!(state, SearchState::OutsideSnippet); 137 | Ok(()) 138 | } 139 | 140 | struct ReadmeInfo { 141 | docs_url: String, 142 | } 143 | 144 | #[derive(Eq, PartialEq)] 145 | enum DecodeState { 146 | Init, 147 | ClosedBracket, 148 | } 149 | 150 | fn readme_transformation(input: &Snippet, info: &ReadmeInfo) -> String { 151 | // Find all doc links, e.g. 152 | // [`set_status`](struct.ActivityId.html#method.set_status) 153 | // and then transform to absolute address 154 | let mut out = String::new(); 155 | let mut state = DecodeState::Init; 156 | for c in input.raw.chars() { 157 | out.push(c); 158 | match c { 159 | ']' => state = DecodeState::ClosedBracket, 160 | '(' => { 161 | if state == DecodeState::ClosedBracket { 162 | out.push_str(&info.docs_url); 163 | } 164 | state = DecodeState::Init; 165 | } 166 | _ => state = DecodeState::Init, 167 | } 168 | } 169 | 170 | out 171 | } 172 | -------------------------------------------------------------------------------- /changes.md: -------------------------------------------------------------------------------- 1 | # Version history with summary of changes 2 | 3 | ## Unreleased 4 | *Crate size: ???* 5 | * New features introduced: 6 | * Subscription handlers can be registered without an activity. (E.g. `nuts::subscribe(|msg: &MyMessage| {...} )`) 7 | 8 | ## 0.2.1 9 | *Crate size: 29.4kB* 10 | * New features introduced: 11 | * More info in logs when running with feature "verbose-debug-log" 12 | * Private messages can now also have subscription masks, allowing activities to receive private messages when inactive. 13 | * Bugfixes: 14 | * Fixed an issue where activities ended up using he same data slots. This could happen when activities were added in subscription handlers multiple times with quiescent moments in between. 15 | * Removed features: 16 | 17 | ## 0.2 18 | *Crate size: 28.3kB* 19 | * New features introduced: 20 | * Essentially, make it possible to do anything anywhere. 21 | Previously, some calls were only allowed from outside callbacks. This restriction has been lifted entirely. 22 | The affected functions are: 23 | * `new_activity` and all variants of it 24 | * `subscribe` and all variants of it 25 | * `on_enter` + `on_leave` 26 | * `store_to_domain` 27 | * `LifecycleStatus::Deleted` (which can be used with `set_status`) and `on_delete` which can take ownership of an activity back outside of nuts. 28 | * `publish_awaiting_response` which returns a future that resolves once the published message has been handled 29 | * Extended debugging support at runtime (when compiled in debug mode) to give useful information when user code dynamically called by nuts panics. 30 | * Supporting messages directed at a single activity through `id.private_channel()` or `id.private_domained_channel()` + `nuts::send_to::()` or `id.private_message()` 31 | * Removed features 32 | * `subscribe_owned` and friends (in favour of private messages which do the same without potential runtime panics) 33 | 34 | ## 0.1.1 35 | *Crate size: 17.7kB* 36 | * Removed dependency (which was only used in examples anyway) 37 | ## 0.1 38 | *Crate size: 22.1kB* 39 | * Initial publication on crates.io 40 | -------------------------------------------------------------------------------- /clippy.toml: -------------------------------------------------------------------------------- 1 | blacklisted-names = [ "foo", "baz", "quux", "num" ] -------------------------------------------------------------------------------- /examples/clicker-game/.appveyor.yml: -------------------------------------------------------------------------------- 1 | install: 2 | - appveyor-retry appveyor DownloadFile https://win.rustup.rs/ -FileName rustup-init.exe 3 | - if not defined RUSTFLAGS rustup-init.exe -y --default-host x86_64-pc-windows-msvc --default-toolchain nightly 4 | - set PATH=%PATH%;C:\Users\appveyor\.cargo\bin 5 | - rustc -V 6 | - cargo -V 7 | 8 | build: false 9 | 10 | test_script: 11 | - cargo test --locked 12 | -------------------------------------------------------------------------------- /examples/clicker-game/.cargo-ok: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakmeier/nuts/d012bb43874d4185a5e14f0d04426ba842c01cb4/examples/clicker-game/.cargo-ok -------------------------------------------------------------------------------- /examples/clicker-game/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | bin/ 5 | pkg/ 6 | wasm-pack.log 7 | -------------------------------------------------------------------------------- /examples/clicker-game/.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | sudo: false 3 | 4 | cache: cargo 5 | 6 | matrix: 7 | include: 8 | 9 | # Builds with wasm-pack. 10 | - rust: beta 11 | env: RUST_BACKTRACE=1 12 | addons: 13 | firefox: latest 14 | chrome: stable 15 | before_script: 16 | - (test -x $HOME/.cargo/bin/cargo-install-update || cargo install cargo-update) 17 | - (test -x $HOME/.cargo/bin/cargo-generate || cargo install --vers "^0.2" cargo-generate) 18 | - cargo install-update -a 19 | - curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh -s -- -f 20 | script: 21 | - cargo generate --git . --name testing 22 | # Having a broken Cargo.toml (in that it has curlies in fields) anywhere 23 | # in any of our parent dirs is problematic. 24 | - mv Cargo.toml Cargo.toml.tmpl 25 | - cd testing 26 | - wasm-pack build 27 | - wasm-pack test --chrome --firefox --headless 28 | 29 | # Builds on nightly. 30 | - rust: nightly 31 | env: RUST_BACKTRACE=1 32 | before_script: 33 | - (test -x $HOME/.cargo/bin/cargo-install-update || cargo install cargo-update) 34 | - (test -x $HOME/.cargo/bin/cargo-generate || cargo install --vers "^0.2" cargo-generate) 35 | - cargo install-update -a 36 | - rustup target add wasm32-unknown-unknown 37 | script: 38 | - cargo generate --git . --name testing 39 | - mv Cargo.toml Cargo.toml.tmpl 40 | - cd testing 41 | - cargo check 42 | - cargo check --target wasm32-unknown-unknown 43 | - cargo check --no-default-features 44 | - cargo check --target wasm32-unknown-unknown --no-default-features 45 | - cargo check --no-default-features --features console_error_panic_hook 46 | - cargo check --target wasm32-unknown-unknown --no-default-features --features console_error_panic_hook 47 | - cargo check --no-default-features --features "console_error_panic_hook wee_alloc" 48 | - cargo check --target wasm32-unknown-unknown --no-default-features --features "console_error_panic_hook wee_alloc" 49 | 50 | # Builds on beta. 51 | - rust: beta 52 | env: RUST_BACKTRACE=1 53 | before_script: 54 | - (test -x $HOME/.cargo/bin/cargo-install-update || cargo install cargo-update) 55 | - (test -x $HOME/.cargo/bin/cargo-generate || cargo install --vers "^0.2" cargo-generate) 56 | - cargo install-update -a 57 | - rustup target add wasm32-unknown-unknown 58 | script: 59 | - cargo generate --git . --name testing 60 | - mv Cargo.toml Cargo.toml.tmpl 61 | - cd testing 62 | - cargo check 63 | - cargo check --target wasm32-unknown-unknown 64 | - cargo check --no-default-features 65 | - cargo check --target wasm32-unknown-unknown --no-default-features 66 | - cargo check --no-default-features --features console_error_panic_hook 67 | - cargo check --target wasm32-unknown-unknown --no-default-features --features console_error_panic_hook 68 | # Note: no enabling the `wee_alloc` feature here because it requires 69 | # nightly for now. 70 | -------------------------------------------------------------------------------- /examples/clicker-game/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | [package] 3 | name = "clicker-game" 4 | version = "0.1.0" 5 | authors = ["Jakob Meier "] 6 | edition = "2018" 7 | 8 | [lib] 9 | crate-type = ["cdylib", "rlib"] 10 | 11 | [features] 12 | default = ["console_error_panic_hook"] 13 | 14 | [dependencies] 15 | wasm-bindgen = "0.2.63" 16 | nuts = { path = "../../" } 17 | 18 | # The `console_error_panic_hook` crate provides better debugging of panics by 19 | # logging them with `console.error`. This is great for development, but requires 20 | # all the `std::fmt` and `std::panicking` infrastructure, so isn't great for 21 | # code size when deploying. 22 | console_error_panic_hook = { version = "0.1.6", optional = true } 23 | 24 | # `wee_alloc` is a tiny allocator for wasm that is only ~1K in code size 25 | # compared to the default allocator's ~10K. It is slower than the default 26 | # allocator, however. 27 | # 28 | # Unfortunately, `wee_alloc` requires nightly Rust when targeting wasm for now. 29 | wee_alloc = { version = "0.4.5", optional = true } 30 | 31 | [dependencies.web-sys] 32 | version = "0.3" 33 | features = [ 34 | "Document", 35 | "Element", 36 | "HtmlCollection", 37 | "Window", 38 | ] 39 | 40 | [dev-dependencies] 41 | wasm-bindgen-test = "0.3.13" 42 | 43 | [profile.release] 44 | # Tell `rustc` to optimize for small code size. 45 | opt-level = "s" 46 | -------------------------------------------------------------------------------- /examples/clicker-game/LICENSE_APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /examples/clicker-game/LICENSE_MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Jakob Meier 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /examples/clicker-game/README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

wasm-pack-template

4 | 5 | A template for kick starting a Rust and WebAssembly project using wasm-pack. 6 | 7 |

8 | Build Status 9 |

10 | 11 |

12 | Tutorial 13 | | 14 | Chat 15 |

16 | 17 | Built with 🦀🕸 by The Rust and WebAssembly Working Group 18 |
19 | 20 | ## About 21 | 22 | [**📚 Read this template tutorial! 📚**][template-docs] 23 | 24 | This template is designed for compiling Rust libraries into WebAssembly and 25 | publishing the resulting package to NPM. 26 | 27 | Be sure to check out [other `wasm-pack` tutorials online][tutorials] for other 28 | templates and usages of `wasm-pack`. 29 | 30 | [tutorials]: https://rustwasm.github.io/docs/wasm-pack/tutorials/index.html 31 | [template-docs]: https://rustwasm.github.io/docs/wasm-pack/tutorials/npm-browser-packages/index.html 32 | 33 | ## 🚴 Usage 34 | 35 | ### 🐑 Use `cargo generate` to Clone this Template 36 | 37 | [Learn more about `cargo generate` here.](https://github.com/ashleygwilliams/cargo-generate) 38 | 39 | ``` 40 | cargo generate --git https://github.com/rustwasm/wasm-pack-template.git --name my-project 41 | cd my-project 42 | ``` 43 | 44 | ### 🛠️ Build with `wasm-pack build` 45 | 46 | ``` 47 | wasm-pack build 48 | ``` 49 | 50 | ### 🔬 Test in Headless Browsers with `wasm-pack test` 51 | 52 | ``` 53 | wasm-pack test --headless --firefox 54 | ``` 55 | 56 | ### 🎁 Publish to NPM with `wasm-pack publish` 57 | 58 | ``` 59 | wasm-pack publish 60 | ``` 61 | 62 | ## 🔋 Batteries Included 63 | 64 | * [`wasm-bindgen`](https://github.com/rustwasm/wasm-bindgen) for communicating 65 | between WebAssembly and JavaScript. 66 | * [`console_error_panic_hook`](https://github.com/rustwasm/console_error_panic_hook) 67 | for logging panic messages to the developer console. 68 | * [`wee_alloc`](https://github.com/rustwasm/wee_alloc), an allocator optimized 69 | for small code size. 70 | -------------------------------------------------------------------------------- /examples/clicker-game/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod utils; 2 | 3 | use wasm_bindgen::prelude::wasm_bindgen; 4 | use web_sys::Element; 5 | 6 | // When the `wee_alloc` feature is enabled, use `wee_alloc` as the global 7 | // allocator. 8 | #[cfg(feature = "wee_alloc")] 9 | #[global_allocator] 10 | static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; 11 | 12 | #[wasm_bindgen] 13 | pub fn init() { 14 | utils::set_panic_hook(); 15 | 16 | let apples = 1; 17 | let trees = 0; 18 | 19 | let window = web_sys::window().unwrap(); 20 | let document = window.document().unwrap(); 21 | let main = document.get_elements_by_tag_name("main").item(0).unwrap(); 22 | let dynamic_text: Element = document.create_element("p").unwrap(); 23 | main.prepend_with_node_1(&dynamic_text).unwrap(); 24 | let game_state = GameState { 25 | dynamic_text, 26 | apples, 27 | trees, 28 | }; 29 | 30 | let game = nuts::new_activity(game_state); 31 | 32 | game.subscribe(GameState::update_text); 33 | game.subscribe(GameState::buy); 34 | game.subscribe(GameState::collect_apples); 35 | nuts::publish(UpdateTextEvent); 36 | 37 | set_timer(); 38 | } 39 | 40 | struct GameState { 41 | dynamic_text: Element, 42 | apples: i32, 43 | trees: i32, 44 | } 45 | 46 | struct UpdateTextEvent; 47 | struct BuyEvent; 48 | struct CollectEvent; 49 | 50 | #[wasm_bindgen] 51 | pub fn buy() { 52 | nuts::publish(BuyEvent); 53 | } 54 | 55 | impl GameState { 56 | fn update_text(&mut self, _: &UpdateTextEvent) { 57 | self.dynamic_text.set_inner_html(&format!( 58 | "You have {} apples and {} trees.", 59 | self.apples, self.trees 60 | )); 61 | } 62 | fn buy(&mut self, _: &BuyEvent) { 63 | if self.apples > 0 { 64 | self.trees += 1; 65 | self.apples -= 1; 66 | nuts::publish(UpdateTextEvent); 67 | } 68 | } 69 | fn collect_apples(&mut self, _: &CollectEvent) { 70 | self.apples += self.trees; 71 | nuts::publish(UpdateTextEvent); 72 | } 73 | } 74 | 75 | use wasm_bindgen::closure::Closure; 76 | use wasm_bindgen::JsCast; 77 | fn set_timer() { 78 | let cb = Closure::wrap(Box::new(|| nuts::publish(CollectEvent)) as Box); 79 | web_sys::window() 80 | .unwrap() 81 | .set_interval_with_callback_and_timeout_and_arguments_0(cb.as_ref().unchecked_ref(), 3000); 82 | 83 | cb.forget(); 84 | } 85 | -------------------------------------------------------------------------------- /examples/clicker-game/src/utils.rs: -------------------------------------------------------------------------------- 1 | pub fn set_panic_hook() { 2 | // When the `console_error_panic_hook` feature is enabled, we can call the 3 | // `set_panic_hook` function at least once during initialization, and then 4 | // we will get better error messages if our code ever panics. 5 | // 6 | // For more details see 7 | // https://github.com/rustwasm/console_error_panic_hook#readme 8 | #[cfg(feature = "console_error_panic_hook")] 9 | console_error_panic_hook::set_once(); 10 | } 11 | -------------------------------------------------------------------------------- /examples/clicker-game/tests/web.rs: -------------------------------------------------------------------------------- 1 | //! Test suite for the Web and headless browsers. 2 | 3 | #![cfg(target_arch = "wasm32")] 4 | 5 | extern crate wasm_bindgen_test; 6 | use wasm_bindgen_test::*; 7 | 8 | wasm_bindgen_test_configure!(run_in_browser); 9 | 10 | #[wasm_bindgen_test] 11 | fn pass() { 12 | assert_eq!(1 + 1, 2); 13 | } 14 | -------------------------------------------------------------------------------- /examples/clicker-game/www/.bin/create-wasm-app.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const { spawn } = require("child_process"); 4 | const fs = require("fs"); 5 | 6 | let folderName = '.'; 7 | 8 | if (process.argv.length >= 3) { 9 | folderName = process.argv[2]; 10 | if (!fs.existsSync(folderName)) { 11 | fs.mkdirSync(folderName); 12 | } 13 | } 14 | 15 | const clone = spawn("git", ["clone", "https://github.com/rustwasm/create-wasm-app.git", folderName]); 16 | 17 | clone.on("close", code => { 18 | if (code !== 0) { 19 | console.error("cloning the template failed!") 20 | process.exit(code); 21 | } else { 22 | console.log("🦀 Rust + 🕸 Wasm = ❤"); 23 | } 24 | }); 25 | -------------------------------------------------------------------------------- /examples/clicker-game/www/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /examples/clicker-game/www/.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: "10" 3 | 4 | script: 5 | - ./node_modules/.bin/webpack 6 | -------------------------------------------------------------------------------- /examples/clicker-game/www/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /examples/clicker-game/www/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) [year] [name] 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /examples/clicker-game/www/README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

create-wasm-app

4 | 5 | An npm init template for kick starting a project that uses NPM packages containing Rust-generated WebAssembly and bundles them with Webpack. 6 | 7 |

8 | Build Status 9 |

10 | 11 |

12 | Usage 13 | | 14 | Chat 15 |

16 | 17 | Built with 🦀🕸 by The Rust and WebAssembly Working Group 18 |
19 | 20 | ## About 21 | 22 | This template is designed for depending on NPM packages that contain 23 | Rust-generated WebAssembly and using them to create a Website. 24 | 25 | * Want to create an NPM package with Rust and WebAssembly? [Check out 26 | `wasm-pack-template`.](https://github.com/rustwasm/wasm-pack-template) 27 | * Want to make a monorepo-style Website without publishing to NPM? Check out 28 | [`rust-webpack-template`](https://github.com/rustwasm/rust-webpack-template) 29 | and/or 30 | [`rust-parcel-template`](https://github.com/rustwasm/rust-parcel-template). 31 | 32 | ## 🚴 Usage 33 | 34 | ``` 35 | npm init wasm-app 36 | ``` 37 | 38 | ## 🔋 Batteries Included 39 | 40 | - `.gitignore`: ignores `node_modules` 41 | - `LICENSE-APACHE` and `LICENSE-MIT`: most Rust projects are licensed this way, so these are included for you 42 | - `README.md`: the file you are reading now! 43 | - `index.html`: a bare bones html document that includes the webpack bundle 44 | - `index.js`: example js file with a comment showing how to import and use a wasm pkg 45 | - `package.json` and `package-lock.json`: 46 | - pulls in devDependencies for using webpack: 47 | - [`webpack`](https://www.npmjs.com/package/webpack) 48 | - [`webpack-cli`](https://www.npmjs.com/package/webpack-cli) 49 | - [`webpack-dev-server`](https://www.npmjs.com/package/webpack-dev-server) 50 | - defines a `start` script to run `webpack-dev-server` 51 | - `webpack.config.js`: configuration file for bundling your js with webpack 52 | 53 | ## License 54 | 55 | Licensed under either of 56 | 57 | * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 58 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 59 | 60 | at your option. 61 | 62 | ### Contribution 63 | 64 | Unless you explicitly state otherwise, any contribution intentionally 65 | submitted for inclusion in the work by you, as defined in the Apache-2.0 66 | license, shall be dual licensed as above, without any additional terms or 67 | conditions. 68 | -------------------------------------------------------------------------------- /examples/clicker-game/www/bootstrap.js: -------------------------------------------------------------------------------- 1 | // A dependency graph that contains any wasm must all be imported 2 | // asynchronously. This `bootstrap.js` file does the single async import, so 3 | // that no one else needs to worry about it again. 4 | import("./index.js") 5 | .catch(e => console.error("Error importing `index.js`:", e)); 6 | -------------------------------------------------------------------------------- /examples/clicker-game/www/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | "Nuts Clicker Game 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |

16 | Awesome Clicker Game 17 |

18 |
19 | 20 |
21 | Plant tree 22 |
23 |
24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /examples/clicker-game/www/index.js: -------------------------------------------------------------------------------- 1 | import * as wasm from "clicker-game"; 2 | import "./styles.css"; 3 | 4 | window.buy = function() { 5 | wasm.buy(); 6 | } 7 | 8 | wasm.init() -------------------------------------------------------------------------------- /examples/clicker-game/www/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nuts-example-clicker-game", 3 | "version": "0.1.0", 4 | "description": "Example clicker game implemented with nuts", 5 | "main": "index.js", 6 | "bin": { 7 | "clicker-game": ".bin/clicker.js" 8 | }, 9 | "scripts": { 10 | "build": "webpack --config webpack.config.js", 11 | "start": "webpack-dev-server" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/jakmeier/nuts.git" 16 | }, 17 | "keywords": [ 18 | "webassembly", 19 | "wasm", 20 | "rust", 21 | "webpack" 22 | ], 23 | "author": "Jakob Meier ", 24 | "license": "(MIT OR Apache-2.0)", 25 | "bugs": { 26 | "url": "https://github.com/jakmeier/nuts/issues" 27 | }, 28 | "dependencies": { 29 | "clicker-game": "file:../pkg" 30 | }, 31 | "devDependencies": { 32 | "copy-webpack-plugin": "^5.0.0", 33 | "css-loader": "^4.3.0", 34 | "hello-wasm-pack": "^0.1.0", 35 | "style-loader": "^1.2.1", 36 | "webpack": "^4.29.3", 37 | "webpack-cli": "^3.1.0", 38 | "webpack-dev-server": "^3.1.5" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /examples/clicker-game/www/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: darkSeaGreen; 3 | font-family: 'Architects Daughter', cursive; 4 | font-size: larger; 5 | text-align: center; 6 | height: 300px; 7 | padding: 50px; 8 | } 9 | 10 | .button { 11 | display: inline-block; 12 | padding: 0.35em 1.2em; 13 | border: 0.1em solid #FFFFFF; 14 | margin: 0 0.3em 0.3em 0; 15 | border-radius: 0.12em; 16 | box-sizing: border-box; 17 | text-decoration: none; 18 | color: #FFFFFF; 19 | background-color: #D2691E; 20 | text-align: center; 21 | transition: all 0.2s; 22 | } -------------------------------------------------------------------------------- /examples/clicker-game/www/webpack.config.js: -------------------------------------------------------------------------------- 1 | const CopyWebpackPlugin = require("copy-webpack-plugin"); 2 | const path = require('path'); 3 | 4 | module.exports = { 5 | entry: "./bootstrap.js", 6 | output: { 7 | path: path.resolve(__dirname, "dist"), 8 | filename: "bootstrap.js", 9 | }, 10 | mode: "development", 11 | plugins: [ 12 | new CopyWebpackPlugin(['index.html']) 13 | ], 14 | module: { 15 | rules: [{ 16 | test: /\.css$/, 17 | use: [ 18 | 'style-loader', 19 | 'css-loader' 20 | ] 21 | }] 22 | } 23 | }; -------------------------------------------------------------------------------- /src/debug.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused_macros)] 2 | /* log_print, to println! or web console log (nothing in release mode) */ 3 | 4 | #[cfg(debug_assertions)] 5 | #[cfg(all(feature = "web-debug", target_arch = "wasm32"))] 6 | macro_rules! log_print { 7 | ( $( $t:tt )* ) => { 8 | web_sys::console::log_1(&format!( $( $t )* ).into()); 9 | } 10 | } 11 | 12 | #[cfg(debug_assertions)] 13 | #[cfg(not(all(feature = "web-debug", target_arch = "wasm32")))] 14 | macro_rules! log_print { 15 | ( $( $t:tt )* ) => { 16 | println!( $( $t )* ); 17 | } 18 | } 19 | 20 | #[cfg(not(debug_assertions))] 21 | macro_rules! log_print { 22 | ( $( $t:tt )* ) => {}; 23 | } 24 | 25 | /* debug_print, to web console debug or (for now) println (could be extended to be configurable) */ 26 | 27 | #[cfg(debug_assertions)] 28 | #[cfg(all(feature = "web-debug", target_arch = "wasm32"))] 29 | macro_rules! debug_print { 30 | ( $( $t:tt )* ) => { 31 | web_sys::console::debug_1(&format!( $( $t )* ).into()); 32 | } 33 | } 34 | 35 | #[cfg(debug_assertions)] 36 | #[cfg(not(all(feature = "web-debug", target_arch = "wasm32")))] 37 | macro_rules! debug_print { 38 | ( $( $t:tt )* ) => { 39 | println!( $( $t )* ); 40 | } 41 | } 42 | 43 | #[cfg(not(debug_assertions))] 44 | macro_rules! debug_print { 45 | ( $( $t:tt )* ) => {}; 46 | } 47 | 48 | #[derive(Clone, Copy)] 49 | pub(crate) struct DebugTypeName( 50 | #[cfg(debug_assertions)] pub(crate) &'static str, 51 | #[cfg(not(debug_assertions))] (), 52 | ); 53 | 54 | impl DebugTypeName { 55 | pub fn new() -> Self { 56 | Self( 57 | #[cfg(debug_assertions)] 58 | std::any::type_name::(), 59 | #[cfg(not(debug_assertions))] 60 | (), 61 | ) 62 | } 63 | } 64 | 65 | #[cfg(debug_assertions)] 66 | impl std::fmt::Debug for DebugTypeName { 67 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 68 | write!(f, "{}", self.0) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // @ START-DOC CRATE 2 | //! Nuts is a library that offers a simple publish-subscribe API, featuring decoupled creation of the publisher and the subscriber. 3 | //! 4 | //! ## Quick first example 5 | //! ```rust 6 | //! struct Activity; 7 | //! let activity = nuts::new_activity(Activity); 8 | //! activity.subscribe( 9 | //! |_activity, n: &usize| 10 | //! println!("Subscriber received {}", n) 11 | //! ); 12 | //! nuts::publish(17usize); 13 | //! // "Subscriber received 17" is printed 14 | //! nuts::publish(289usize); 15 | //! // "Subscriber received 289" is printed 16 | //! ``` 17 | //! 18 | //! As you can see in the example above, no explicit channel between publisher and subscriber is necessary. 19 | //! The call to `publish` is a static method that requires no state from the user. 20 | //! The connection between them is implicit because both use `usize` as message type. 21 | //! 22 | //! Nuts enables this simple API by managing all necessary state in thread-local storage. 23 | //! This is particularly useful when targeting the web. However, Nuts can be used on other platforms, too. 24 | //! In fact, Nuts has no dependencies aside from std. 25 | // @ END-DOC CRATE 26 | 27 | // code quality 28 | #![forbid(unsafe_code)] 29 | #![deny(clippy::mem_forget)] 30 | #![deny(clippy::print_stdout)] 31 | #![warn(clippy::mutex_integer)] 32 | #![warn(clippy::needless_borrow)] 33 | #![warn(clippy::needless_pass_by_value)] 34 | #![warn(clippy::unwrap_used)] 35 | // docs 36 | #![warn(missing_docs)] 37 | #![warn(clippy::doc_markdown)] 38 | #![warn(clippy::missing_errors_doc)] 39 | #![allow(clippy::needless_doctest_main)] 40 | 41 | #[macro_use] 42 | pub(crate) mod debug; 43 | 44 | mod nut; 45 | 46 | #[cfg(test)] 47 | mod test; 48 | 49 | pub use crate::nut::iac::managed_state::{DefaultDomain, DomainEnumeration, DomainState}; 50 | use core::any::Any; 51 | pub use nut::activity::*; 52 | pub use nut::iac::filter::*; 53 | 54 | use nut::iac::managed_state::*; 55 | use nut::iac::topic::*; 56 | 57 | /// Consumes a struct and registers it as an Activity. 58 | /// 59 | /// `nuts::new_activity(...)` is the simplest method to create a new activity. 60 | /// It takes only a single argument, which can be any struct instance or primitive. 61 | /// This object will be the private data for the activity. 62 | /// 63 | /// An `ActivityId` is returned, which is a handle to the newly registered activity. 64 | /// Use it to register callbacks on the activity. 65 | /// 66 | /// ### Example: 67 | // @ START-DOC NEW_ACTIVITY 68 | /// ```rust 69 | /// #[derive(Default)] 70 | /// struct MyActivity { 71 | /// round: usize 72 | /// } 73 | /// struct MyMessage { 74 | /// no: usize 75 | /// } 76 | /// 77 | /// // Create activity 78 | /// let activity = MyActivity::default(); 79 | /// // Activity moves into globally managed state, ID to handle it is returned 80 | /// let activity_id = nuts::new_activity(activity); 81 | /// 82 | /// // Add event listener that listens to published `MyMessage` types 83 | /// activity_id.subscribe( 84 | /// |my_activity, msg: &MyMessage| { 85 | /// println!("Round: {}, Message No: {}", my_activity.round, msg.no); 86 | /// my_activity.round += 1; 87 | /// } 88 | /// ); 89 | /// 90 | /// // prints "Round: 0, Message No: 1" 91 | /// nuts::publish( MyMessage { no: 1 } ); 92 | /// // prints "Round: 1, Message No: 2" 93 | /// nuts::publish( MyMessage { no: 2 } ); 94 | /// ``` 95 | // @ END-DOC NEW_ACTIVITY 96 | pub fn new_activity(activity: A) -> ActivityId 97 | where 98 | A: Activity, 99 | { 100 | let a = nut::new_activity(activity, DomainId::default(), LifecycleStatus::Active); 101 | #[cfg(feature = "verbose-debug-log")] 102 | debug_print!( 103 | "New activity {:?}({})", 104 | std::any::type_name::(), 105 | a.id.index 106 | ); 107 | a 108 | } 109 | 110 | /// Consumes a struct that is registered as an Activity that has access to the specified domain. 111 | /// Use the returned `ActivityId` to register callbacks on the activity. 112 | /// 113 | // @ START-DOC NEW_ACTIVITY_WITH_DOMAIN 114 | /// ```rust 115 | /// use nuts::{domain_enum, DomainEnumeration}; 116 | /// 117 | /// #[derive(Default)] 118 | /// struct MyActivity; 119 | /// struct MyMessage; 120 | /// 121 | /// #[derive(Clone, Copy)] 122 | /// enum MyDomain { 123 | /// DomainA, 124 | /// DomainB, 125 | /// } 126 | /// domain_enum!(MyDomain); 127 | /// 128 | /// // Add data to domain 129 | /// nuts::store_to_domain(&MyDomain::DomainA, 42usize); 130 | /// 131 | /// // Register activity 132 | /// let activity_id = nuts::new_domained_activity(MyActivity, &MyDomain::DomainA); 133 | /// 134 | /// // Add event listener that listens to published `MyMessage` types and has also access to the domain data 135 | /// activity_id.subscribe_domained( 136 | /// |_my_activity, domain, msg: &MyMessage| { 137 | /// // borrow data from the domain 138 | /// let data = domain.try_get::(); 139 | /// assert_eq!(*data.unwrap(), 42); 140 | /// } 141 | /// ); 142 | /// 143 | /// // make sure the subscription closure is called 144 | /// nuts::publish( MyMessage ); 145 | /// ``` 146 | // @ END-DOC NEW_ACTIVITY_WITH_DOMAIN 147 | pub fn new_domained_activity(activity: A, domain: &D) -> ActivityId 148 | where 149 | A: Activity, 150 | D: DomainEnumeration, 151 | { 152 | let a = nut::new_activity(activity, DomainId::new(domain), LifecycleStatus::Active); 153 | #[cfg(feature = "verbose-debug-log")] 154 | debug_print!( 155 | "New activity {:?}({})", 156 | std::any::type_name::(), 157 | a.id.index 158 | ); 159 | 160 | a 161 | } 162 | 163 | /// Puts the data object to the domain, which can be accessed by all associated activities. 164 | /// 165 | /// This function stores the data to the domain immediately if called outside of activities. 166 | /// Inside activities, it will be delayed. However, any messages published after calling this function can 167 | /// rely on the store to the domain to have completed when the corresponding subscribers are executed. 168 | pub fn store_to_domain(domain: &D, data: T) 169 | where 170 | D: DomainEnumeration, 171 | T: core::any::Any, 172 | { 173 | nut::write_domain(domain, data) 174 | } 175 | 176 | /// Registers a callback closure with a specific topic to listen to. 177 | /// 178 | /// This variant of subscription has no activity. See [`ActivityId::subscribe`](struct.ActivityId.html#method.subscribe) and friends for other subscription options. 179 | pub fn subscribe(f: F) 180 | where 181 | F: Fn(&MSG) + 'static, 182 | MSG: Any, 183 | { 184 | crate::nut::register_no_activity(f) 185 | } 186 | 187 | /// Send the message to all subscribed activities 188 | /// 189 | // @ START-DOC PUBLISH 190 | /// Any instance of a struct or primitive can be published, as long as its type is known at compile-time. (The same constraint as for Activities.) 191 | /// Upon calling `nuts::publish`, all active subscriptions for the same type are executed and the published object will be shared with all of them. 192 | /// 193 | /// ### Example 194 | /// ```rust 195 | /// struct ChangeUser { user_name: String } 196 | /// pub fn main() { 197 | /// let msg = ChangeUser { user_name: "Donald Duck".to_owned() }; 198 | /// nuts::publish(msg); 199 | /// // Subscribers to messages of type `ChangeUser` will be notified 200 | /// } 201 | /// ``` 202 | // @ END-DOC PUBLISH 203 | /// ### Advanced: Understanding the Execution Order 204 | // @ START-DOC PUBLISH_ADVANCED 205 | /// When calling `nuts::publish(...)`, the message may not always be published immediately. While executing a subscription handler from previous `publish`, all new messages are queued up until the previous one is completed. 206 | /// ```rust 207 | /// struct MyActivity; 208 | /// let activity = nuts::new_activity(MyActivity); 209 | /// activity.subscribe( 210 | /// |_, msg: &usize| { 211 | /// println!("Start of {}", msg); 212 | /// if *msg < 3 { 213 | /// nuts::publish( msg + 1 ); 214 | /// } 215 | /// println!("End of {}", msg); 216 | /// } 217 | /// ); 218 | /// 219 | /// nuts::publish(0usize); 220 | /// // Output: 221 | /// // Start of 0 222 | /// // End of 0 223 | /// // Start of 1 224 | /// // End of 1 225 | /// // Start of 2 226 | /// // End of 2 227 | /// // Start of 3 228 | /// // End of 3 229 | /// ``` 230 | // @ END-DOC PUBLISH_ADVANCED 231 | pub fn publish(a: A) { 232 | nut::publish_custom(a) 233 | } 234 | 235 | /// Returns a future of type `NutsResponse` which will resolve after the 236 | /// message has been published and all subscribers have finished processing it. 237 | pub async fn publish_awaiting_response(a: A) { 238 | nut::publish_custom_and_await(a).await; 239 | } 240 | 241 | /// Publish a message to a specific activity. The same as `id.private_message()` but works without an `ActivityId`. 242 | /// 243 | /// The first type parameter must always be specified. 244 | /// It determines the receiver of the message. 245 | /// The message is ignored silently if no such activity has been registered or if it has no private channel for this message. 246 | /// 247 | /// The second type parameter can usually be deferred by the compiler, it is the type of the message to be sent. 248 | /// ### Example 249 | // @ START-DOC PUBLISH_PRIVATE 250 | /// ```rust 251 | /// struct ExampleActivity {} 252 | /// let id = nuts::new_activity(ExampleActivity {}); 253 | /// // `private_channel` works similar to `subscribe` but it owns the message. 254 | /// id.private_channel(|_activity, msg: usize| { 255 | /// assert_eq!(msg, 7); 256 | /// }); 257 | /// // `send_to` must be used instead of `publish` when using private channels. 258 | /// // Which activity receives the message is decide by the first type parameter. 259 | /// nuts::send_to::(7usize); 260 | /// ``` 261 | // @ END-DOC PUBLISH_PRIVATE 262 | pub fn send_to(msg: MSG) { 263 | nut::send_custom::(msg) 264 | } 265 | 266 | #[cfg(debug_assertions)] 267 | /// Read some information about currently processing activities. 268 | /// This should be called inside a panic hook. 269 | /// 270 | /// This function is only available in debug mode as a runtime cost is associated with recording the necessary data at all times. 271 | /// The correct flag for conditional compilation is `#[cfg(debug_assertions)]`. 272 | /// 273 | /// # Example 274 | /// ``` 275 | /// fn add_nuts_hook() { 276 | /// #[cfg(debug_assertions)] 277 | /// let previous_hook = std::panic::take_hook(); 278 | /// std::panic::set_hook(Box::new(move |panic_info| { 279 | /// let nuts_info = nuts::panic_info(); 280 | /// #[cfg(features = "web-debug")] 281 | /// web_sys::console::error_1(&nuts_info.into()); 282 | /// #[cfg(not(features = "web-debug"))] 283 | /// eprintln!("{}", nuts_info); 284 | /// previous_hook(panic_info) 285 | /// })); 286 | /// } 287 | /// ``` 288 | pub fn panic_info() -> String { 289 | nut::nuts_panic_info() 290 | .unwrap_or_else(|| "NUTS panic hook: Failed to read panic info.".to_owned()) 291 | } 292 | -------------------------------------------------------------------------------- /src/nut.rs: -------------------------------------------------------------------------------- 1 | //! Top-level module for all the inner-magic of nuts. 2 | //! 3 | //! Nothing in here is public interface but documentation is still important for 4 | //! library developers as well as users if they want to understand more how this library works. 5 | 6 | pub(crate) mod activity; 7 | pub(crate) mod exec; 8 | pub(crate) mod iac; 9 | 10 | use crate::nut::exec::Deferred; 11 | use crate::nut::iac::subscription::OnDelete; 12 | use crate::*; 13 | use crate::{debug::DebugTypeName, nut::exec::inchoate::InchoateActivityContainer}; 14 | use core::any::Any; 15 | use core::sync::atomic::AtomicBool; 16 | use exec::fifo::ThreadLocalFifo; 17 | use iac::managed_state::*; 18 | use std::cell::RefCell; 19 | 20 | use self::iac::{ 21 | publish::{BroadcastInfo, ResponseTracker}, 22 | subscription::Subscriptions, 23 | }; 24 | 25 | thread_local!(static NUT: Nut = Nut::new()); 26 | 27 | pub(crate) const IMPOSSIBLE_ERR_MSG: &str = 28 | "Bug in nuts. It should be impossible to trigger this panic through any combinations of library calls."; 29 | 30 | /// A nut stores thread-local state and provides an easy interface to access it. 31 | /// 32 | /// To allow nested access to the nut, it is a read-only structure. 33 | /// The field of it can be accessed separately. The library is designed carefully to 34 | /// ensure single-write/multiple-reader is enforced at all times. 35 | #[derive(Default)] 36 | struct Nut { 37 | /// Stores the data for activities, the semi-isolated components of this library. 38 | /// Mutable access given on each closure dispatch. 39 | activities: RefCell, 40 | /// Keeps state necessary for inter-activity communication. (domain state and message slot) 41 | /// Mutable access given on each closure dispatch. 42 | managed_state: RefCell, 43 | /// Closures sorted by topic. 44 | /// Mutable access only from outside of handlers, preferably before first publish call. 45 | /// Read-only access afterwards. 46 | /// (This restriction might change in the future) 47 | subscriptions: Subscriptions, 48 | /// FIFO queue for published messages and other events that cannot be processed immediately. 49 | /// Atomically accessed mutably between closure dispatches. 50 | deferred_events: ThreadLocalFifo, 51 | /// Tracks awaited responses, which are pending futures. 52 | /// Used when creating new futures (NutsResponse) and when polling the same. 53 | /// Atomically accessed in with_response_tracker_mut() only. 54 | response_tracker: RefCell, 55 | /// A flag that marks if a broadcast is currently on-going 56 | executing: AtomicBool, 57 | /// When executing a broadcast, `activities` and `managed_state` is not available. 58 | /// To still be able to add new activities during that time, temporary 59 | /// structures are used to buffer additions. Theses are then merged in a deferred event. 60 | /// (Note: Adding subscriptions does not require additional structure because they will 61 | /// be queued and only executed after the activity is available anyway) 62 | inchoate_activities: RefCell, 63 | /// For debugging messages 64 | #[allow(dead_code)] 65 | active_activity_name: std::cell::Cell>, 66 | } 67 | 68 | /// A method that can be called by the `ActivityManager`. 69 | /// These handlers are created by the library and not part of the public interface. 70 | pub(crate) type Handler = Box; 71 | 72 | impl Nut { 73 | fn new() -> Self { 74 | Self { 75 | activities: RefCell::new(ActivityContainer::new()), 76 | ..Default::default() 77 | } 78 | } 79 | fn quiescent(&self) -> bool { 80 | !self.executing.load(std::sync::atomic::Ordering::Relaxed) 81 | } 82 | fn add_on_delete(&self, id: UncheckedActivityId, subscription: OnDelete) { 83 | if self.quiescent() { 84 | self.activities 85 | .try_borrow_mut() 86 | .expect(IMPOSSIBLE_ERR_MSG) 87 | .add_on_delete(id, subscription); 88 | } else { 89 | self.deferred_events 90 | .push(Deferred::OnDeleteSubscription(id, subscription)) 91 | } 92 | } 93 | pub(crate) fn with_response_tracker_mut(f: impl FnOnce(&mut ResponseTracker) -> T) -> T { 94 | NUT.with(|nut| { 95 | let mut response_tracker = nut 96 | .response_tracker 97 | .try_borrow_mut() 98 | .expect(IMPOSSIBLE_ERR_MSG); 99 | f(&mut *response_tracker) 100 | }) 101 | } 102 | } 103 | 104 | pub(crate) fn new_activity( 105 | activity: A, 106 | domain_index: DomainId, 107 | status: LifecycleStatus, 108 | ) -> ActivityId 109 | where 110 | A: Activity, 111 | { 112 | NUT.with(|nut| { 113 | // When already executing, the state is already borrowed. 114 | // In that case, we have to defer creation to a quiescent state. 115 | // In the other case, we are guaranteed to have access. 116 | if !nut.executing.load(std::sync::atomic::Ordering::Relaxed) { 117 | // Make sure domain are allocated. 118 | // This is currently necessary on every new_activity call, which is a bit ugly. 119 | // On the other hand, performance of creating new activities is only secondary priority. 120 | nut.managed_state 121 | .try_borrow_mut() 122 | .expect(IMPOSSIBLE_ERR_MSG) 123 | .prepare(domain_index); 124 | // Make sure that length of activities is available without locking activities. 125 | // Again, a bit ugly but performance is secondary in this call. 126 | nut.inchoate_activities 127 | .try_borrow_mut() 128 | .expect(IMPOSSIBLE_ERR_MSG) 129 | .inc_offset(); 130 | nut.activities 131 | .try_borrow_mut() 132 | .expect(IMPOSSIBLE_ERR_MSG) 133 | .add(activity, domain_index, status) 134 | } else { 135 | nut.deferred_events.push(Deferred::FlushInchoateActivities); 136 | let a = nut 137 | .inchoate_activities 138 | .try_borrow_mut() 139 | .expect(IMPOSSIBLE_ERR_MSG) 140 | .add(activity, domain_index, status); 141 | #[cfg(feature = "verbose-debug-log")] 142 | #[cfg(debug_assertions)] 143 | debug_print!( 144 | "Added activity as inchoate. ID = {} ({} complete activities, {} inchoate activities exist)", 145 | a.id.index, 146 | nut.inchoate_activities.try_borrow().expect(IMPOSSIBLE_ERR_MSG).offset(), 147 | nut.inchoate_activities.try_borrow().expect(IMPOSSIBLE_ERR_MSG).len() - 1 148 | ); 149 | a 150 | } 151 | }) 152 | } 153 | 154 | pub(crate) fn publish_custom(a: MSG) { 155 | NUT.with(|nut| nut.broadcast(BroadcastInfo::global(a, Topic::public_message::()))) 156 | } 157 | 158 | pub(crate) fn send_custom(a: MSG) { 159 | NUT.with(|nut| { 160 | nut.broadcast(BroadcastInfo::local_by_type::( 161 | a, 162 | Topic::private_message::(), 163 | )) 164 | }) 165 | } 166 | 167 | pub(crate) fn send_custom_by_id(msg: MSG, id: UncheckedActivityId) { 168 | NUT.with(|nut| { 169 | nut.broadcast(BroadcastInfo::local( 170 | msg, 171 | id, 172 | Topic::private_message::(), 173 | )) 174 | }) 175 | } 176 | 177 | pub(crate) async fn publish_custom_and_await(a: A) { 178 | NUT.with(move |nut| nut.publish_and_await(a)).await; 179 | } 180 | 181 | pub(crate) fn register_no_activity(f: F) 182 | where 183 | F: Fn(&MSG) + 'static, 184 | MSG: Any, 185 | { 186 | NUT.with(|nut| { 187 | let closure = ManagedState::pack_closure_no_activity::<_, MSG>(f); 188 | let topic = Topic::public_message::(); 189 | let id = NotAnActivity::id(); 190 | nut.push_closure(topic, id, closure); 191 | }); 192 | } 193 | pub(crate) fn register(id: ActivityId, f: F, filter: SubscriptionFilter) 194 | where 195 | A: Activity, 196 | F: Fn(&mut A, &MSG) + 'static, 197 | MSG: Any, 198 | { 199 | NUT.with(|nut| { 200 | let closure = ManagedState::pack_closure::<_, _, MSG>(f, id, filter); 201 | let topic = Topic::public_message::(); 202 | nut.push_closure(topic, id, closure); 203 | }); 204 | } 205 | pub(crate) fn register_mut(id: ActivityId, f: F, filter: SubscriptionFilter) 206 | where 207 | A: Activity, 208 | F: Fn(&mut A, &mut MSG) + 'static, 209 | MSG: Any, 210 | { 211 | NUT.with(|nut| { 212 | let closure = ManagedState::pack_closure_mut::<_, _, MSG>(f, id, filter); 213 | let topic = Topic::public_message::(); 214 | nut.push_closure(topic, id, closure); 215 | }); 216 | } 217 | pub(crate) fn register_owned(id: ActivityId, f: F, filter: SubscriptionFilter) 218 | where 219 | A: Activity, 220 | F: Fn(&mut A, MSG) + 'static, 221 | MSG: Any, 222 | { 223 | NUT.with(|nut| { 224 | let closure = ManagedState::pack_closure_owned::<_, _, MSG>(f, id, filter); 225 | let topic = Topic::private_message::(); 226 | nut.push_closure(topic, id, closure); 227 | }); 228 | } 229 | 230 | /// For subscriptions without payload 231 | pub(crate) fn register_no_payload( 232 | id: ActivityId, 233 | f: F, 234 | topic: Topic, 235 | filter: SubscriptionFilter, 236 | ) where 237 | A: Activity, 238 | F: Fn(&mut A) + 'static, 239 | { 240 | NUT.with(|nut| { 241 | let closure = ManagedState::pack_closure_no_payload(f, id, filter); 242 | nut.push_closure(topic, id, closure); 243 | }); 244 | } 245 | 246 | pub(crate) fn register_domained(id: ActivityId, f: F, filter: SubscriptionFilter) 247 | where 248 | A: Activity, 249 | F: Fn(&mut A, &mut DomainState, &MSG) + 'static, 250 | MSG: Any, 251 | { 252 | NUT.with(|nut| { 253 | let closure = ManagedState::pack_domained_closure(f, id, filter); 254 | let topic = Topic::public_message::(); 255 | nut.push_closure(topic, id, closure); 256 | }); 257 | } 258 | pub(crate) fn register_domained_mut(id: ActivityId, f: F, filter: SubscriptionFilter) 259 | where 260 | A: Activity, 261 | F: Fn(&mut A, &mut DomainState, &mut MSG) + 'static, 262 | MSG: Any, 263 | { 264 | NUT.with(|nut| { 265 | let closure = ManagedState::pack_domained_closure_mut(f, id, filter); 266 | let topic = Topic::public_message::(); 267 | nut.push_closure(topic, id, closure); 268 | }); 269 | } 270 | pub(crate) fn register_domained_owned( 271 | id: ActivityId, 272 | f: F, 273 | filter: SubscriptionFilter, 274 | ) where 275 | A: Activity, 276 | F: Fn(&mut A, &mut DomainState, MSG) + 'static, 277 | MSG: Any, 278 | { 279 | NUT.with(|nut| { 280 | let closure = ManagedState::pack_domained_closure_owned(f, id, filter); 281 | let topic = Topic::private_message::(); 282 | nut.push_closure(topic, id, closure); 283 | }); 284 | } 285 | 286 | /// For subscriptions without payload but with domain access 287 | pub(crate) fn register_domained_no_payload( 288 | id: ActivityId, 289 | f: F, 290 | topic: Topic, 291 | filter: SubscriptionFilter, 292 | ) where 293 | A: Activity, 294 | F: Fn(&mut A, &mut DomainState) + 'static, 295 | { 296 | NUT.with(|nut| { 297 | let closure = ManagedState::pack_closure_domained_no_payload(f, id, filter); 298 | nut.push_closure(topic, id, closure); 299 | }); 300 | } 301 | 302 | pub(crate) fn register_on_delete(id: ActivityId, f: F) 303 | where 304 | A: Activity, 305 | F: FnOnce(A) + 'static, 306 | { 307 | NUT.with(|nut| { 308 | let closure = Box::new(|a: Box| { 309 | let activity = a.downcast().expect(IMPOSSIBLE_ERR_MSG); 310 | f(*activity); 311 | }); 312 | nut.add_on_delete(id.into(), OnDelete::Simple(closure)); 313 | }) 314 | } 315 | 316 | pub(crate) fn register_domained_on_delete(id: ActivityId, f: F) 317 | where 318 | A: Activity, 319 | F: FnOnce(A, &mut DomainState) + 'static, 320 | { 321 | NUT.with(|nut| { 322 | let closure = Box::new(move |a: Box, managed_state: &mut ManagedState| { 323 | let activity = a.downcast().expect(IMPOSSIBLE_ERR_MSG); 324 | let domain = managed_state 325 | .get_mut(id.domain_index) 326 | .expect("missing domain"); 327 | f(*activity, domain); 328 | }); 329 | let subscription = OnDelete::WithDomain(closure); 330 | nut.add_on_delete(id.into(), subscription); 331 | }) 332 | } 333 | 334 | pub(crate) fn set_status(id: UncheckedActivityId, status: LifecycleStatus) { 335 | NUT.with(|nut| nut.set_status(id, status)); 336 | } 337 | 338 | pub(crate) fn write_domain(domain: &D, data: T) 339 | where 340 | D: DomainEnumeration, 341 | T: core::any::Any, 342 | { 343 | NUT.with(|nut| { 344 | let id = DomainId::new(domain); 345 | if let Ok(mut managed_state) = nut.managed_state.try_borrow_mut() { 346 | managed_state.prepare(id); 347 | let storage = managed_state.get_mut(id).expect("No domain"); 348 | storage.store(data); 349 | } else { 350 | let event = Deferred::DomainStore(DomainStoreData::new(id, data)); 351 | nut.deferred_events.push(event); 352 | } 353 | }) 354 | } 355 | 356 | #[cfg(debug_assertions)] 357 | pub(crate) fn nuts_panic_info() -> Option { 358 | NUT.try_with(|nut| { 359 | let mut info = String::new(); 360 | info.push_str("NUTS panic hook: Panicked while "); 361 | if let Some(activity) = nut.active_activity_name.get() { 362 | info.push_str(activity.0); 363 | } else { 364 | info.push_str("no"); 365 | } 366 | info.push_str(" activity was active.\n"); 367 | info 368 | }) 369 | .ok() 370 | } 371 | -------------------------------------------------------------------------------- /src/nut/activity.rs: -------------------------------------------------------------------------------- 1 | mod activity_container; 2 | mod lifecycle; 3 | 4 | pub(crate) use activity_container::*; 5 | pub use lifecycle::*; 6 | 7 | use crate::nut::iac::{filter::SubscriptionFilter, managed_state::DomainId}; 8 | use crate::*; 9 | use core::any::Any; 10 | use std::ops::{Index, IndexMut}; 11 | 12 | // @ START-DOC ACTIVITY 13 | /// Activities are at the core of Nuts. 14 | /// From the globally managed data, they represent the active part, i.e. they can have event listeners. 15 | /// The passive counter-part is defined by `DomainState`. 16 | /// 17 | /// Every struct that has a type with static lifetime (anything that has no lifetime parameter that is determined only at runtime) can be used as an Activity. 18 | /// You don't have to implement the `Activity` trait yourself, it will always be automatically derived if possible. 19 | /// 20 | /// To create an activity, simply register the object that should be used as activity, using `nuts::new_activity` or one of its variants. 21 | /// 22 | /// It is important to understand that Activities are uniquely defined by their type. 23 | /// You cannot create two activities from the same type. (But you can, for example, create a wrapper type around it.) 24 | /// This allows activities to be referenced by their type, which must be known at run-time. 25 | // @ END-DOC ACTIVITY 26 | pub trait Activity: Any {} 27 | impl Activity for T {} 28 | 29 | #[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Hash)] 30 | /// Handle to an `Activity` that has been registered, with a type parameter to track the activity's type. 31 | /// Can be used to add type-checked closures to the activity, which will be used as event listeners. 32 | /// 33 | /// Implements `Copy` and `Clone` 34 | pub struct ActivityId { 35 | pub(crate) id: UncheckedActivityId, 36 | pub(crate) domain_index: DomainId, 37 | phantom: std::marker::PhantomData, 38 | } 39 | 40 | /// This type is used for subscriptions without activity. It is zero sized, hence should be a zero-cost abstraction. 41 | pub(crate) struct NotAnActivity; 42 | 43 | #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)] 44 | /// Pointer to an activity that has been registered. 45 | /// Can be used to set the lifecycle stats of activities. 46 | /// 47 | /// The information about the activity's type is lost at this point. 48 | /// Therefore, this id cannot be used to register closures. 49 | pub struct UncheckedActivityId { 50 | pub(crate) index: usize, 51 | } 52 | 53 | impl ActivityId { 54 | pub(crate) fn new(index: usize, domain_index: DomainId) -> Self { 55 | Self { 56 | id: UncheckedActivityId { index }, 57 | domain_index, 58 | phantom: Default::default(), 59 | } 60 | } 61 | /// Registers a callback closure that is called when an activity changes from inactive to active. 62 | /// Multiple handlers can be registered. 63 | pub fn on_enter(&self, f: F) 64 | where 65 | F: Fn(&mut A) + 'static, 66 | { 67 | crate::nut::register_no_payload(*self, f, Topic::enter(), SubscriptionFilter::no_filter()) 68 | } 69 | /// Same as `on_enter` but with domain access in closure 70 | pub fn on_enter_domained(&self, f: F) 71 | where 72 | F: Fn(&mut A, &mut DomainState) + 'static, 73 | { 74 | crate::nut::register_domained_no_payload( 75 | *self, 76 | f, 77 | Topic::enter(), 78 | SubscriptionFilter::no_filter(), 79 | ) 80 | } 81 | /// Registers a callback closure that is called when an activity changes from active to inactive. 82 | /// Multiple handlers can be registered. 83 | pub fn on_leave(&self, f: F) 84 | where 85 | F: Fn(&mut A) + 'static, 86 | { 87 | crate::nut::register_no_payload(*self, f, Topic::leave(), SubscriptionFilter::no_filter()) 88 | } 89 | /// Same as `on_leave` but with domain access in closure 90 | pub fn on_leave_domained(&self, f: F) 91 | where 92 | F: Fn(&mut A, &mut DomainState) + 'static, 93 | { 94 | crate::nut::register_domained_no_payload( 95 | *self, 96 | f, 97 | Topic::leave(), 98 | SubscriptionFilter::no_filter(), 99 | ) 100 | } 101 | /// Registers a callback closure that is called when an activity is deleted. 102 | /// Only one handler can be registered because it takes ownership of the data. 103 | /// A second registration will overwrite the first handler. 104 | pub fn on_delete(&self, f: F) 105 | where 106 | F: FnOnce(A) + 'static, 107 | { 108 | crate::nut::register_on_delete(*self, f); 109 | } 110 | /// Same as `on_delete` but with domain access in closure 111 | pub fn on_delete_domained(&self, f: F) 112 | where 113 | F: FnOnce(A, &mut DomainState) + 'static, 114 | { 115 | crate::nut::register_domained_on_delete(*self, f); 116 | } 117 | 118 | /// Registers a callback closure on an activity with a specific topic to listen to. 119 | /// 120 | /// By default, the activity will only receive calls when it is active. 121 | /// Use `subscribe_masked` for more control over this behavior. 122 | /// 123 | /// ### Example 124 | // @ START-DOC SUBSCRIBE_EXAMPLE 125 | /// ```rust 126 | /// struct MyActivity { id: usize }; 127 | /// struct MyMessage { text: String }; 128 | /// 129 | /// pub fn main() { 130 | /// let activity = nuts::new_activity(MyActivity { id: 0 } ); 131 | /// activity.subscribe( 132 | /// |activity: &mut MyActivity, message: &MyMessage| 133 | /// println!("Subscriber with ID {} received text: {}", activity.id, message.text) 134 | /// ); 135 | /// } 136 | /// ``` 137 | /// In the example above, a subscription is created that waits for messages of type `MyMessage` to be published. 138 | /// So far, the code inside the closure is not executed and nothing is printed to the console. 139 | /// 140 | /// Note that the first argument of the closure is a mutable reference to the activity object. 141 | /// The second argument is a read-only reference to the published message. 142 | /// Both types must match exactly or otherwise the closure will not be accepted by the compiler. 143 | /// 144 | /// A function with the correct argument types can also be used to subscribe. 145 | /// ```rust 146 | /// struct MyActivity { id: usize }; 147 | /// struct MyMessage { text: String }; 148 | /// 149 | /// pub fn main() { 150 | /// let activity = nuts::new_activity(MyActivity { id: 0 } ); 151 | /// activity.subscribe(MyActivity::print_text); 152 | /// } 153 | /// 154 | /// impl MyActivity { 155 | /// fn print_text(&mut self, message: &MyMessage) { 156 | /// println!("Subscriber with ID {} received text: {}", self.id, message.text) 157 | /// } 158 | /// } 159 | /// ``` 160 | // @ END-DOC SUBSCRIBE_EXAMPLE 161 | pub fn subscribe(&self, f: F) 162 | where 163 | F: Fn(&mut A, &MSG) + 'static, 164 | MSG: Any, 165 | { 166 | crate::nut::register(*self, f, Default::default()) 167 | } 168 | /// Same as [subscribe](#method.subscribe) but gives mutable access to the message object. 169 | /// 170 | /// Make sure to use the correct signature for the function, the Rust compiler may give strange error messages otherwise. 171 | /// For example, the message must be borrowed by the subscription handler. 172 | pub fn subscribe_mut(&self, f: F) 173 | where 174 | F: Fn(&mut A, &mut MSG) + 'static, 175 | MSG: Any, 176 | { 177 | crate::nut::register_mut(*self, f, Default::default()) 178 | } 179 | 180 | /// Registers a callback closure on an activity with a specific topic to listen to. 181 | /// Has mutable access to the `DomainState` object. 182 | /// 183 | /// By default, the activity will only receive calls when it is active. 184 | /// Use `subscribe_domained_masked` for more control over this behavior. 185 | /// 186 | /// Make sure to use the correct signature for the function, the Rust compiler may give strange error messages otherwise. 187 | /// For example, the message must be borrowed by the subscription handler. 188 | /// 189 | /// # Panics 190 | /// Panics if the activity has not been registered with a domain. 191 | pub fn subscribe_domained(&self, f: F) 192 | where 193 | F: Fn(&mut A, &mut DomainState, &MSG) + 'static, 194 | MSG: Any, 195 | { 196 | crate::nut::register_domained(*self, f, Default::default()) 197 | } 198 | /// Same as [`subscribe_domained`](#method.subscribe_domained) but gives mutable access to the message object. 199 | pub fn subscribe_domained_mut(&self, f: F) 200 | where 201 | F: Fn(&mut A, &mut DomainState, &mut MSG) + 'static, 202 | MSG: Any, 203 | { 204 | crate::nut::register_domained_mut(*self, f, Default::default()) 205 | } 206 | 207 | /// Registers a callback closure on an activity with a specific topic to listen to. 208 | /// Messages sent with `nuts::publish()` are NOT received, only messages sent with `nuts::send_to()`. 209 | /// 210 | /// Attention! The handler takes ownership of the message. 211 | /// It will compile if it is borrowed instead, but then it will also expect a reference to be published. (Which usually doesn't work due to lifetimes) 212 | /// Then, it will not react to normally sent messages and can be difficult to debug. 213 | /// 214 | /// Since the listener takes ownership, it is not possible to have more than one private channel active for the same activity at the same time. 215 | /// If multiple private channels are added to an activity, only the last listener is retained. (Older ones are replaced and deleted) 216 | pub fn private_channel(&self, f: F) 217 | where 218 | F: Fn(&mut A, MSG) + 'static, 219 | MSG: Any, 220 | { 221 | crate::nut::register_owned(*self, f, Default::default()) 222 | } 223 | 224 | /// Variant of `private_channel` with access to the domain state. 225 | /// 226 | /// # Panics 227 | /// Panics if the activity has not been registered with a domain. 228 | pub fn private_domained_channel(&self, f: F) 229 | where 230 | F: Fn(&mut A, &mut DomainState, MSG) + 'static, 231 | MSG: Any, 232 | { 233 | crate::nut::register_domained_owned(*self, f, Default::default()) 234 | } 235 | 236 | /// Variant of `private_channel` with subscription mask. 237 | pub fn private_channel_masked(&self, mask: SubscriptionFilter, f: F) 238 | where 239 | F: Fn(&mut A, MSG) + 'static, 240 | MSG: Any, 241 | { 242 | crate::nut::register_owned(*self, f, mask) 243 | } 244 | 245 | /// Variant of `private_channel` with access to the domain state and subscription mask. 246 | /// 247 | /// # Panics 248 | /// Panics if the activity has not been registered with a domain. 249 | pub fn private_domained_channel_masked(&self, mask: SubscriptionFilter, f: F) 250 | where 251 | F: Fn(&mut A, &mut DomainState, MSG) + 'static, 252 | MSG: Any, 253 | { 254 | crate::nut::register_domained_owned(*self, f, mask) 255 | } 256 | 257 | /// Registers a callback closure on an activity with a specific topic to listen to with filtering options. 258 | pub fn subscribe_masked(&self, mask: SubscriptionFilter, f: F) 259 | where 260 | F: Fn(&mut A, &MSG) + 'static, 261 | MSG: Any, 262 | { 263 | crate::nut::register(*self, f, mask) 264 | } 265 | /// Same as [`subscribe_masked`](#method.subscribe_masked) but gives mutable access to the message object. 266 | pub fn subscribe_masked_mut(&self, mask: SubscriptionFilter, f: F) 267 | where 268 | F: Fn(&mut A, &mut MSG) + 'static, 269 | MSG: Any, 270 | { 271 | crate::nut::register_mut(*self, f, mask) 272 | } 273 | 274 | /// Registers a callback closure on an activity with a specific topic to listen to with filtering options. 275 | /// Has mutable access to the `DomainState` object. 276 | /// 277 | /// # Panics 278 | /// Panics if the activity has not been registered with a domain. 279 | pub fn subscribe_domained_masked(&self, mask: SubscriptionFilter, f: F) 280 | where 281 | F: Fn(&mut A, &mut DomainState, &MSG) + 'static, 282 | MSG: Any, 283 | { 284 | crate::nut::register_domained(*self, f, mask) 285 | } 286 | /// Same as [`subscribe_domained_masked`](#method.subscribe_domained_masked) but gives mutable access to the message object. 287 | pub fn subscribe_domained_masked_mut(&self, mask: SubscriptionFilter, f: F) 288 | where 289 | F: Fn(&mut A, &mut DomainState, &mut MSG) + 'static, 290 | MSG: Any, 291 | { 292 | crate::nut::register_domained_mut(*self, f, mask) 293 | } 294 | 295 | /// Changes the lifecycle status of the activity 296 | /// 297 | /// # Panics 298 | /// If status is set to something other than Deleted after it has been Deleted 299 | pub fn set_status(&self, status: LifecycleStatus) { 300 | crate::nut::set_status((*self).into(), status); 301 | } 302 | 303 | /// Publish a message to a specific activity. 304 | /// 305 | /// If you lack access to an `ActivityId`, use `nuts::send_to()` or `UncheckedActivityId::private_message`. 306 | /// Both are equivalent. 307 | pub fn private_message(&self, msg: MSG) { 308 | let id: UncheckedActivityId = (*self).into(); 309 | id.private_message(msg); 310 | } 311 | } 312 | 313 | impl UncheckedActivityId { 314 | /// Changes the lifecycle status of the activity 315 | /// 316 | /// # Panics 317 | /// If status is set to something other than Deleted after it has been Deleted 318 | pub fn set_status(&self, status: LifecycleStatus) { 319 | crate::nut::set_status(*self, status); 320 | } 321 | /// Publish a message to a specific activity. 322 | /// 323 | /// If you lack access to an `UncheckedActivityId`, use `nuts::send_to()`, it is equivalent. 324 | pub fn private_message(&self, msg: A) { 325 | nut::send_custom_by_id(msg, *self) 326 | } 327 | /// A unique number for the activity. 328 | /// Can be used for serialization in combination with `forge_from_usize` 329 | pub fn as_usize(&self) -> usize { 330 | self.index 331 | } 332 | /// Can be used for deserialization in combination with `as_usize` 333 | /// 334 | /// If used in any other way, you might experience panics. 335 | /// Right now, there should still be no UB but that might change in future versions. 336 | pub fn forge_from_usize(index: usize) -> Self { 337 | Self { index } 338 | } 339 | } 340 | 341 | impl NotAnActivity { 342 | pub fn id() -> ActivityId { 343 | ActivityId::::new(0, DomainId::default()) 344 | } 345 | } 346 | 347 | impl Copy for ActivityId {} 348 | impl Clone for ActivityId { 349 | fn clone(&self) -> Self { 350 | *self 351 | } 352 | } 353 | 354 | impl Into for ActivityId { 355 | fn into(self) -> UncheckedActivityId { 356 | self.id 357 | } 358 | } 359 | -------------------------------------------------------------------------------- /src/nut/activity/activity_container.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use crate::nut::iac::subscription::OnDelete; 3 | use core::any::TypeId; 4 | 5 | /// A collection of heterogenous Activities 6 | /// 7 | /// Needs stores a list of dynamic `Any` trait objects, not `Activity` because 8 | /// trait objects only allow access to methods of that trait, not their super-traits. 9 | #[derive(Default)] 10 | pub(crate) struct ActivityContainer { 11 | data: Vec>>, 12 | active: Vec, 13 | on_delete: Vec, 14 | } 15 | 16 | impl ActivityContainer { 17 | pub(crate) fn new() -> Self { 18 | Self { 19 | data: vec![Some(Box::new(NotAnActivity))], 20 | active: vec![LifecycleStatus::Active], 21 | on_delete: vec![OnDelete::None], 22 | } 23 | } 24 | pub(crate) fn add( 25 | &mut self, 26 | a: A, 27 | domain: DomainId, 28 | status: LifecycleStatus, 29 | ) -> ActivityId { 30 | let i = self.data.len(); 31 | self.data.push(Some(Box::new(a))); 32 | self.active.push(status); 33 | self.on_delete.push(OnDelete::None); 34 | ActivityId::new(i, domain) 35 | } 36 | pub(crate) fn status(&self, id: UncheckedActivityId) -> LifecycleStatus { 37 | self.active[id.index] 38 | } 39 | pub(crate) fn set_status(&mut self, id: UncheckedActivityId, status: LifecycleStatus) { 40 | self.active[id.index] = status 41 | } 42 | pub(crate) fn add_on_delete(&mut self, id: UncheckedActivityId, f: OnDelete) { 43 | self.on_delete[id.index] = f; 44 | } 45 | pub(crate) fn delete(&mut self, id: UncheckedActivityId, managed_state: &mut ManagedState) { 46 | if let Some(activity) = self.data[id.index].take() { 47 | // Taking ownership to call FnOnce 48 | let mut on_delete = OnDelete::None; 49 | std::mem::swap(&mut on_delete, &mut self.on_delete[id.index]); 50 | match on_delete { 51 | OnDelete::None => { /* NOP */ } 52 | OnDelete::Simple(f) => f(activity), 53 | OnDelete::WithDomain(f) => f(activity, managed_state), 54 | } 55 | } 56 | } 57 | pub(crate) fn len(&self) -> usize { 58 | self.data.len() 59 | } 60 | 61 | pub(crate) fn append(&mut self, other: &mut Self) { 62 | self.active.append(&mut other.active); 63 | self.data.append(&mut other.data); 64 | self.on_delete.append(&mut other.on_delete); 65 | } 66 | pub(crate) fn id_lookup(&self, t: TypeId) -> Option { 67 | // This is not the most efficient (if there are many activities) but it does the job to get something working. 68 | // If anyone ever find this to be a performance bottleneck in a real application, this can be fixed with some smarter implementation. 69 | #[allow(clippy::unwrap_used)] 70 | if let Some(index) = self.data.iter().position(|maybe_activity| { 71 | maybe_activity.is_some() && (*maybe_activity.as_ref().unwrap().as_ref()).type_id() == t 72 | }) { 73 | Some(UncheckedActivityId { index }) 74 | } else { 75 | None 76 | } 77 | } 78 | } 79 | 80 | impl Index> for ActivityContainer { 81 | type Output = dyn Any; 82 | fn index(&self, id: ActivityId) -> &Self::Output { 83 | self.data[id.id.index] 84 | .as_ref() 85 | .expect("Missing activity") 86 | .as_ref() 87 | } 88 | } 89 | impl IndexMut> for ActivityContainer { 90 | fn index_mut(&mut self, id: ActivityId) -> &mut Self::Output { 91 | self.data[id.id.index] 92 | .as_mut() 93 | .expect("Missing activity") 94 | .as_mut() 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/nut/activity/lifecycle.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use crate::nut::{iac::publish::BroadcastInfo, Nut, IMPOSSIBLE_ERR_MSG}; 3 | 4 | // @ START-DOC ACTIVITY_LIFECYCLE 5 | /// Each activity has a lifecycle status that can be changed using [`set_status`](struct.ActivityId.html#method.set_status). 6 | /// It starts with `LifecycleStatus::Active`. 7 | /// In the current version of Nuts, the only other status is `LifecycleStatus::Inactive`. 8 | /// 9 | /// The inactive status can be used to put activities to sleep temporarily. 10 | /// While inactive, the activity will not be notified of events it has subscribed to. 11 | /// A subscription filter can been used to change this behavior. 12 | /// (See [`subscribe_masked`](struct.ActivityId.html#method.subscribe_masked)) 13 | /// 14 | /// If the status of a changes from active to inactive, the activity's [`on_leave`](struct.ActivityId.html#method.on_leave) and [`on_leave_domained`](struct.ActivityId.html#method.on_leave_domained) subscriptions will be called. 15 | /// 16 | /// If the status of a changes from inactive to active, the activity's [`on_enter`](struct.ActivityId.html#method.on_enter) and [`on_enter_domained`](struct.ActivityId.html#method.on_enter_domained) subscriptions will be called. 17 | /// 18 | // @ END-DOC ACTIVITY_LIFECYCLE 19 | #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] 20 | #[non_exhaustive] 21 | pub enum LifecycleStatus { 22 | /// The normal status. Every Activity starts with this status. 23 | Active, 24 | /// Inactive / Sleeping 25 | Inactive, 26 | /// Mark for deletion, the activity will be removed and `on_delete` called on it. 27 | /// Setting to this state twice will cause panics. 28 | Deleted, 29 | } 30 | 31 | pub(crate) struct LifecycleChange { 32 | activity: UncheckedActivityId, 33 | status: LifecycleStatus, 34 | } 35 | 36 | impl LifecycleStatus { 37 | /// Returns true iff the status is one that is considered to be active. (Only `LifecycleStatus::Active` at the moment) 38 | /// 39 | /// Use this method instead of a `==` comparison with `LifecycleStatus::Active` 40 | /// to keep compatibility with future versions of Nuts that may add other status variants. 41 | pub fn is_active(&self) -> bool { 42 | match self { 43 | Self::Active => true, 44 | Self::Inactive => false, 45 | Self::Deleted => false, 46 | } 47 | } 48 | } 49 | 50 | impl Nut { 51 | pub(crate) fn set_status(&self, id: UncheckedActivityId, status: LifecycleStatus) { 52 | let event = LifecycleChange { 53 | activity: id, 54 | status, 55 | }; 56 | self.deferred_events.push(event.into()); 57 | self.catch_up_deferred_to_quiescence(); 58 | } 59 | /// only access after locking with executing flag 60 | pub(crate) fn unchecked_lifecycle_change(&self, lifecycle_change: &LifecycleChange) { 61 | let before = self 62 | .activities 63 | .try_borrow() 64 | .expect(IMPOSSIBLE_ERR_MSG) 65 | .status(lifecycle_change.activity); 66 | if before != lifecycle_change.status { 67 | assert_ne!( 68 | before, 69 | LifecycleStatus::Deleted, 70 | "Attempted to set activity {} status after it has been deleted.", 71 | lifecycle_change.activity.index 72 | ); 73 | self.activities 74 | .try_borrow_mut() 75 | .expect(IMPOSSIBLE_ERR_MSG) 76 | .set_status(lifecycle_change.activity, lifecycle_change.status); 77 | if !before.is_active() && lifecycle_change.status.is_active() { 78 | self.broadcast(BroadcastInfo::local( 79 | (), 80 | lifecycle_change.activity, 81 | Topic::enter(), 82 | )); 83 | } else if before.is_active() && !lifecycle_change.status.is_active() { 84 | self.broadcast(BroadcastInfo::local( 85 | (), 86 | lifecycle_change.activity, 87 | Topic::leave(), 88 | )); 89 | } 90 | } 91 | if lifecycle_change.status == LifecycleStatus::Deleted { 92 | // Delete must be deferred in case the on_leave is hanging. 93 | self.deferred_events 94 | .push(nut::exec::Deferred::RemoveActivity( 95 | lifecycle_change.activity, 96 | )); 97 | } 98 | } 99 | pub(crate) fn delete_activity(&self, id: UncheckedActivityId) { 100 | self.activities 101 | .try_borrow_mut() 102 | .expect(IMPOSSIBLE_ERR_MSG) 103 | .delete( 104 | id, 105 | &mut self 106 | .managed_state 107 | .try_borrow_mut() 108 | .expect(IMPOSSIBLE_ERR_MSG), 109 | ); 110 | } 111 | } 112 | 113 | #[cfg(debug_assertions)] 114 | impl std::fmt::Debug for LifecycleChange { 115 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 116 | write!(f, "Transition to state: {:?}", self.status) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/nut/exec.rs: -------------------------------------------------------------------------------- 1 | use crate::nut::activity::LifecycleChange; 2 | use crate::nut::iac::publish::{BroadcastInfo, ResponseSlot}; 3 | use crate::nut::Nut; 4 | use crate::DomainStoreData; 5 | use crate::UncheckedActivityId; 6 | 7 | pub(crate) mod fifo; 8 | pub(crate) mod inchoate; 9 | 10 | pub(crate) enum Deferred { 11 | Broadcast(BroadcastInfo), 12 | BroadcastAwaitingResponse(BroadcastInfo, ResponseSlot), 13 | Subscription(NewSubscription), 14 | OnDeleteSubscription(UncheckedActivityId, OnDelete), 15 | LifecycleChange(LifecycleChange), 16 | RemoveActivity(UncheckedActivityId), 17 | DomainStore(DomainStoreData), 18 | FlushInchoateActivities, 19 | } 20 | use core::sync::atomic::Ordering; 21 | 22 | use super::{ 23 | iac::subscription::{NewSubscription, OnDelete}, 24 | IMPOSSIBLE_ERR_MSG, 25 | }; 26 | 27 | impl Nut { 28 | /// Delivers all queue broadcasts (or other events) and all newly added broadcasts during that time period. 29 | /// 30 | /// If this is called in at a point of quiescence (no messages in flight) 31 | /// it will return also in such a point. (Queued messages are not in flight.) 32 | /// 33 | /// No guarantee is given for calls while a broadcast is ongoing (messages are in flight). 34 | /// It is perfectly valid (and the intended behavior) to do nothing when called while a executing already. 35 | pub(crate) fn catch_up_deferred_to_quiescence(&self) { 36 | // A Nut only allows single-threaded access, relaxed ordering is fine. 37 | if !self.executing.swap(true, Ordering::Relaxed) { 38 | #[cfg(feature = "verbose-debug-log")] 39 | debug_print!("Start Executing from quiescent moment"); 40 | self.unchecked_catch_up_deferred_to_quiescence(); 41 | self.executing.store(false, Ordering::Relaxed); 42 | #[cfg(feature = "verbose-debug-log")] 43 | debug_print!("Quiescence Reached"); 44 | } 45 | } 46 | 47 | /// only access after locking with executing flag 48 | fn unchecked_catch_up_deferred_to_quiescence(&self) { 49 | while let Some(deferred) = self.deferred_events.pop() { 50 | #[cfg(debug_assertions)] 51 | let debug_message = format!("Executing: {:?}", deferred); 52 | 53 | #[cfg(feature = "verbose-debug-log")] 54 | #[cfg(debug_assertions)] 55 | debug_print!("{}", debug_message); 56 | 57 | #[cfg(feature = "verbose-debug-log")] 58 | #[cfg(debug_assertions)] 59 | if self.deferred_events.len() > 0 { 60 | let events = self.deferred_events.events_debug_list(); 61 | debug_print!( 62 | "{} more events in queue: {}", 63 | self.deferred_events.len(), 64 | events 65 | ); 66 | } 67 | 68 | #[cfg(not(debug_assertions))] 69 | self.exec_deferred(deferred); 70 | 71 | // Catch panics inside executed closures 72 | // Unfortunately, this currently does not seem to work on the web. 73 | // To have good web debugging, the nuts panic hook should be used. 74 | #[cfg(debug_assertions)] 75 | if let Err(panic_info) = 76 | std::panic::catch_unwind(std::panic::AssertUnwindSafe(move || { 77 | self.exec_deferred(deferred) 78 | })) 79 | { 80 | log_print!("Panic ocurred while nuts was executing. {}", debug_message); 81 | log_print!( 82 | "Activity executing right now: {:?}", 83 | self.active_activity_name.get() 84 | ); 85 | std::panic::resume_unwind(panic_info); 86 | } 87 | } 88 | } 89 | fn exec_deferred(&self, deferred: Deferred) { 90 | match deferred { 91 | Deferred::Broadcast(b) => self.unchecked_broadcast(b), 92 | Deferred::BroadcastAwaitingResponse(b, slot) => { 93 | self.unchecked_broadcast(b); 94 | Nut::with_response_tracker_mut(|rt| rt.done(&slot)); 95 | } 96 | Deferred::Subscription(sub) => { 97 | self.subscriptions.exec_new_subscription(sub); 98 | } 99 | Deferred::OnDeleteSubscription(id, sub) => { 100 | self.activities 101 | .try_borrow_mut() 102 | .expect(IMPOSSIBLE_ERR_MSG) 103 | .add_on_delete(id, sub); 104 | } 105 | Deferred::LifecycleChange(lc) => self.unchecked_lifecycle_change(&lc), 106 | Deferred::RemoveActivity(id) => self.delete_activity(id), 107 | Deferred::DomainStore(d) => self.exec_domain_store(d), 108 | Deferred::FlushInchoateActivities => self 109 | .inchoate_activities 110 | .try_borrow_mut() 111 | .expect(IMPOSSIBLE_ERR_MSG) 112 | .flush(&mut *self.activities.try_borrow_mut().expect(IMPOSSIBLE_ERR_MSG)), 113 | } 114 | } 115 | } 116 | impl Into for BroadcastInfo { 117 | fn into(self) -> Deferred { 118 | Deferred::Broadcast(self) 119 | } 120 | } 121 | 122 | impl Into for LifecycleChange { 123 | fn into(self) -> Deferred { 124 | Deferred::LifecycleChange(self) 125 | } 126 | } 127 | 128 | #[cfg(debug_assertions)] 129 | impl std::fmt::Debug for Deferred { 130 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 131 | match self { 132 | Self::Broadcast(b) => write!(f, "Broadcasting {:?}", b), 133 | Self::BroadcastAwaitingResponse(b, _rs) => write!(f, "Broadcasting {:?}", b), 134 | Self::Subscription(sub) => write!(f, "{:?}", sub), 135 | Self::OnDeleteSubscription(_id, _) => { 136 | write!(f, "Adding new on delete listener {}", _id.index) 137 | } 138 | Self::LifecycleChange(lc) => write!(f, "{:?}", lc), 139 | Self::RemoveActivity(_id) => write!(f, "Delete activity {}.", _id.index), 140 | Self::DomainStore(ds) => write!(f, "{:?}", ds), 141 | Self::FlushInchoateActivities => write!(f, "Adding new activities previously deferred"), 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/nut/exec/fifo.rs: -------------------------------------------------------------------------------- 1 | use core::cell::RefCell; 2 | use std::collections::VecDeque; 3 | 4 | /// FIFO queue that allows thread-local atomic pushing and popping. 5 | /// No borrowing of internal data is possible, only moving data in and out. 6 | /// No mutable access required for those operation. 7 | /// 8 | /// Note that the chosen limitation prevents an implementation of Iterator for 9 | /// this collection. `IntoIterator` would be possible but is mostly useless. 10 | pub(crate) struct ThreadLocalFifo { 11 | fifo: RefCell>, 12 | } 13 | 14 | impl ThreadLocalFifo { 15 | pub(crate) fn push(&self, i: ITEM) { 16 | self.fifo.borrow_mut().push_back(i); 17 | } 18 | pub(crate) fn pop(&self) -> Option { 19 | self.fifo.borrow_mut().pop_front() 20 | } 21 | #[cfg(feature = "verbose-debug-log")] 22 | pub(crate) fn len(&self) -> usize { 23 | self.fifo.borrow().len() 24 | } 25 | } 26 | 27 | impl ThreadLocalFifo { 28 | #[cfg(feature = "verbose-debug-log")] 29 | pub(crate) fn events_debug_list(&self) -> String { 30 | let mut out = "(".to_owned(); 31 | for e in self.fifo.borrow().iter() { 32 | out += &format!("{:?}, ", e); 33 | } 34 | if out.len() > 2 { 35 | out.remove(out.len() - 1); 36 | out.remove(out.len() - 1); 37 | } 38 | out += ")"; 39 | out 40 | } 41 | } 42 | impl Default for ThreadLocalFifo { 43 | fn default() -> Self { 44 | ThreadLocalFifo { 45 | fifo: RefCell::new(VecDeque::new()), 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/nut/exec/inchoate.rs: -------------------------------------------------------------------------------- 1 | //! When executing a broadcast, `activities` and `managed_state` is not available. 2 | //! To still be able to add new activities and subscriptions during that time, temporary 3 | //! structures are used to buffer additions. Theses are then merged in a deferred event. 4 | 5 | use crate::{Activity, ActivityContainer, ActivityId, DomainId, LifecycleStatus}; 6 | 7 | pub(crate) struct InchoateActivityContainer { 8 | activities: ActivityContainer, 9 | offset: usize, 10 | } 11 | 12 | impl Default for InchoateActivityContainer { 13 | fn default() -> Self { 14 | Self { 15 | activities: Default::default(), 16 | offset: 1, // for NotAnActivity 17 | } 18 | } 19 | } 20 | 21 | impl InchoateActivityContainer { 22 | pub(crate) fn inc_offset(&mut self) { 23 | debug_assert_eq!(self.activities.len(), 0); 24 | self.offset += 1; 25 | } 26 | #[cfg(feature = "verbose-debug-log")] 27 | pub(crate) fn offset(&self) -> usize { 28 | self.offset 29 | } 30 | pub(crate) fn len(&self) -> usize { 31 | self.activities.len() 32 | } 33 | pub(crate) fn flush(&mut self, final_activities: &mut ActivityContainer) { 34 | self.offset += self.len(); 35 | final_activities.append(&mut self.activities); 36 | } 37 | } 38 | 39 | // Delegation impl 40 | impl InchoateActivityContainer { 41 | pub(crate) fn add( 42 | &mut self, 43 | a: A, 44 | domain: DomainId, 45 | status: LifecycleStatus, 46 | ) -> ActivityId { 47 | let mut aid = self.activities.add(a, domain, status); 48 | aid.id.index += self.offset; 49 | aid 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/nut/iac.rs: -------------------------------------------------------------------------------- 1 | //! Inter Activity Communication 2 | //! 3 | //! This module contains all the glue necessary for activities to work together. 4 | //! 5 | //! A publish-subscribe model is used for scheduling, notifications, and low-bandwidth message bandwidth. 6 | //! 7 | //! TODO: model for shared memory is planned for higher bandwidth communication. 8 | 9 | pub(crate) mod filter; 10 | pub(crate) mod managed_state; 11 | pub(crate) mod publish; 12 | pub(crate) mod subscription; 13 | pub(crate) mod topic; 14 | -------------------------------------------------------------------------------- /src/nut/iac/filter.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | /// Defines under which circumstances a subscribing activity should be called. 4 | /// At the moment, the only filter option is to check the activity lifecycle state. 5 | /// The default filter will ignore events when the activity is inactive. 6 | #[derive(Debug, Clone)] 7 | #[non_exhaustive] 8 | pub struct SubscriptionFilter { 9 | /// Only call the subscribed closure when the activity is active. 10 | pub active_only: bool, 11 | } 12 | 13 | impl Default for SubscriptionFilter { 14 | fn default() -> Self { 15 | Self { active_only: true } 16 | } 17 | } 18 | 19 | impl SubscriptionFilter { 20 | /// Create a new subscription filter that will ensure the activity always receives a message, even when inactive. 21 | pub fn no_filter() -> Self { 22 | Self { active_only: false } 23 | } 24 | } 25 | 26 | impl ActivityContainer { 27 | /// Returns true if the call should go through (false if it should be filtered out) 28 | pub(crate) fn filter( 29 | &self, 30 | id: ActivityId, 31 | filter: &SubscriptionFilter, 32 | ) -> bool { 33 | !filter.active_only || self.status(id.id).is_active() 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/nut/iac/managed_state.rs: -------------------------------------------------------------------------------- 1 | //! Managed State 2 | //! 3 | //! Objects to which multiple activities have access 4 | 5 | mod domain_id; 6 | mod domain_state; 7 | mod domain_store; 8 | 9 | use crate::nut::activity::Activity; 10 | use crate::nut::activity::ActivityContainer; 11 | use crate::nut::activity::ActivityId; 12 | use crate::nut::iac::filter::SubscriptionFilter; 13 | use crate::nut::Handler; 14 | use crate::nut::IMPOSSIBLE_ERR_MSG; 15 | use core::any::Any; 16 | pub use domain_id::*; 17 | pub use domain_state::*; 18 | pub(crate) use domain_store::*; 19 | 20 | #[derive(Default)] 21 | pub(crate) struct ManagedState { 22 | domains: Vec, 23 | broadcast: Option>, 24 | } 25 | 26 | impl ManagedState { 27 | pub(crate) fn get_mut(&mut self, id: DomainId) -> Option<&mut DomainState> { 28 | id.index().map(move |i| &mut self.domains[i]) 29 | } 30 | /// Fills all domains with default values. Must be called once or will panic when used. 31 | pub(crate) fn prepare(&mut self, id: DomainId) { 32 | if let Some(n) = id.index() { 33 | while self.domains.len() <= n { 34 | self.domains.push(Default::default()); 35 | } 36 | } 37 | } 38 | pub(crate) fn set_broadcast(&mut self, msg: Box) { 39 | self.broadcast = Some(msg); 40 | } 41 | pub(crate) fn clear_broadcast(&mut self) { 42 | self.broadcast = None; 43 | } 44 | /// panics if runtime broadcast is not of static type A 45 | fn current_broadcast(&mut self) -> &mut A { 46 | let msg = self 47 | .broadcast 48 | .as_mut() 49 | .expect(IMPOSSIBLE_ERR_MSG) 50 | .downcast_mut() 51 | .expect(IMPOSSIBLE_ERR_MSG); 52 | msg 53 | } 54 | fn current_broadcast_and_domain(&mut self, id: DomainId) -> (&mut A, &mut DomainState) { 55 | let msg: &mut A = self 56 | .broadcast 57 | .as_mut() 58 | .expect(IMPOSSIBLE_ERR_MSG) 59 | .downcast_mut() 60 | .expect(IMPOSSIBLE_ERR_MSG); 61 | let i = id.index().expect(IMPOSSIBLE_ERR_MSG); 62 | let domain = &mut self.domains[i]; 63 | (msg, domain) 64 | } 65 | fn take_current_broadcast(&mut self) -> Box { 66 | let msg = self 67 | .broadcast 68 | .take() 69 | .expect(IMPOSSIBLE_ERR_MSG) 70 | .downcast() 71 | .expect(IMPOSSIBLE_ERR_MSG); 72 | msg 73 | } 74 | fn take_current_broadcast_and_borrow_domain( 75 | &mut self, 76 | id: DomainId, 77 | ) -> (Box, &mut DomainState) { 78 | let msg = self 79 | .broadcast 80 | .take() 81 | .expect(IMPOSSIBLE_ERR_MSG) 82 | .downcast() 83 | .expect(IMPOSSIBLE_ERR_MSG); 84 | let i = id.index().expect("Activity has no domain"); 85 | let domain = &mut self.domains[i]; 86 | (msg, domain) 87 | } 88 | 89 | pub(crate) fn pack_closure_no_payload( 90 | f: F, 91 | index: ActivityId, 92 | filter: SubscriptionFilter, 93 | ) -> Handler 94 | where 95 | A: Activity, 96 | F: Fn(&mut A) + 'static, 97 | { 98 | Box::new( 99 | move |activities: &mut ActivityContainer, _: &mut ManagedState| { 100 | if activities.filter(index, &filter) { 101 | let a = activities[index] 102 | .downcast_mut::() 103 | .expect(IMPOSSIBLE_ERR_MSG); 104 | f(a) 105 | } 106 | }, 107 | ) 108 | } 109 | 110 | pub(crate) fn pack_closure_domained_no_payload( 111 | f: F, 112 | index: ActivityId, 113 | filter: SubscriptionFilter, 114 | ) -> Handler 115 | where 116 | A: Activity, 117 | F: Fn(&mut A, &mut DomainState) + 'static, 118 | { 119 | Box::new( 120 | move |activities: &mut ActivityContainer, managed_state: &mut ManagedState| { 121 | if activities.filter(index, &filter) { 122 | let a = activities[index] 123 | .downcast_mut::() 124 | .expect(IMPOSSIBLE_ERR_MSG); 125 | let domain = &mut managed_state.domains 126 | [index.domain_index.index().expect(IMPOSSIBLE_ERR_MSG)]; 127 | f(a, domain) 128 | } 129 | }, 130 | ) 131 | } 132 | 133 | pub(crate) fn pack_closure_no_activity(f: F) -> Handler 134 | where 135 | F: Fn(&MSG) + 'static, 136 | MSG: Any, 137 | { 138 | Box::new( 139 | move |_activities: &mut ActivityContainer, managed_state: &mut ManagedState| { 140 | let msg = managed_state.current_broadcast(); 141 | f(msg) 142 | }, 143 | ) 144 | } 145 | pub(crate) fn pack_closure( 146 | f: F, 147 | index: ActivityId, 148 | filter: SubscriptionFilter, 149 | ) -> Handler 150 | where 151 | A: Activity, 152 | F: Fn(&mut A, &MSG) + 'static, 153 | MSG: Any, 154 | { 155 | Box::new( 156 | move |activities: &mut ActivityContainer, managed_state: &mut ManagedState| { 157 | if activities.filter(index, &filter) { 158 | let a = activities[index] 159 | .downcast_mut::() 160 | .expect(IMPOSSIBLE_ERR_MSG); 161 | let msg = managed_state.current_broadcast(); 162 | f(a, msg) 163 | } 164 | }, 165 | ) 166 | } 167 | pub(crate) fn pack_closure_mut( 168 | f: F, 169 | index: ActivityId, 170 | filter: SubscriptionFilter, 171 | ) -> Handler 172 | where 173 | A: Activity, 174 | F: Fn(&mut A, &mut MSG) + 'static, 175 | MSG: Any, 176 | { 177 | Box::new( 178 | move |activities: &mut ActivityContainer, managed_state: &mut ManagedState| { 179 | if activities.filter(index, &filter) { 180 | let a = activities[index] 181 | .downcast_mut::() 182 | .expect(IMPOSSIBLE_ERR_MSG); 183 | let msg = managed_state.current_broadcast(); 184 | f(a, msg) 185 | } 186 | }, 187 | ) 188 | } 189 | pub(crate) fn pack_closure_owned( 190 | f: F, 191 | index: ActivityId, 192 | filter: SubscriptionFilter, 193 | ) -> Handler 194 | where 195 | A: Activity, 196 | F: Fn(&mut A, MSG) + 'static, 197 | MSG: Any, 198 | { 199 | Box::new( 200 | move |activities: &mut ActivityContainer, managed_state: &mut ManagedState| { 201 | if activities.filter(index, &filter) { 202 | let a = activities[index] 203 | .downcast_mut::() 204 | .expect(IMPOSSIBLE_ERR_MSG); 205 | let msg = managed_state.take_current_broadcast(); 206 | f(a, *msg) 207 | } 208 | }, 209 | ) 210 | } 211 | pub(crate) fn pack_domained_closure( 212 | f: F, 213 | index: ActivityId, 214 | filter: SubscriptionFilter, 215 | ) -> Handler 216 | where 217 | A: Activity, 218 | F: Fn(&mut A, &mut DomainState, &MSG) + 'static, 219 | MSG: Any, 220 | { 221 | Box::new( 222 | move |activities: &mut ActivityContainer, managed_state: &mut ManagedState| { 223 | if activities.filter(index, &filter) { 224 | let a = activities[index] 225 | .downcast_mut::() 226 | .expect(IMPOSSIBLE_ERR_MSG); 227 | let (msg, domain) = 228 | managed_state.current_broadcast_and_domain(index.domain_index); 229 | f(a, domain, msg) 230 | } 231 | }, 232 | ) 233 | } 234 | pub(crate) fn pack_domained_closure_mut( 235 | f: F, 236 | index: ActivityId, 237 | filter: SubscriptionFilter, 238 | ) -> Handler 239 | where 240 | A: Activity, 241 | F: Fn(&mut A, &mut DomainState, &mut MSG) + 'static, 242 | MSG: Any, 243 | { 244 | Box::new( 245 | move |activities: &mut ActivityContainer, managed_state: &mut ManagedState| { 246 | if activities.filter(index, &filter) { 247 | let a = activities[index] 248 | .downcast_mut::() 249 | .expect(IMPOSSIBLE_ERR_MSG); 250 | let (msg, domain) = 251 | managed_state.current_broadcast_and_domain(index.domain_index); 252 | f(a, domain, msg) 253 | } 254 | }, 255 | ) 256 | } 257 | pub(crate) fn pack_domained_closure_owned( 258 | f: F, 259 | index: ActivityId, 260 | filter: SubscriptionFilter, 261 | ) -> Handler 262 | where 263 | A: Activity, 264 | F: Fn(&mut A, &mut DomainState, MSG) + 'static, 265 | MSG: Any, 266 | { 267 | Box::new( 268 | move |activities: &mut ActivityContainer, managed_state: &mut ManagedState| { 269 | if activities.filter(index, &filter) { 270 | let a = activities[index] 271 | .downcast_mut::() 272 | .expect(IMPOSSIBLE_ERR_MSG); 273 | let (msg, domain) = 274 | managed_state.take_current_broadcast_and_borrow_domain(index.domain_index); 275 | f(a, domain, *msg) 276 | } 277 | }, 278 | ) 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /src/nut/iac/managed_state/domain_id.rs: -------------------------------------------------------------------------------- 1 | #[derive(Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)] 2 | pub(crate) struct DomainId(Option); 3 | 4 | /// Used for mapping domain identifiers to unique integers. 5 | /// Can be derived with `domain_enum!(TYPE)`; 6 | pub trait DomainEnumeration { 7 | /// The unique integer for a specific domain 8 | fn id(&self) -> usize; 9 | } 10 | 11 | /// If only one domain is required, this can be used. 12 | /// But if you have your own domain type defined with `domain_enum!` or implementing `DomainEnumeration` manually, do not use this. 13 | /// Different domain types should never be mixed! 14 | pub struct DefaultDomain; 15 | 16 | impl DomainEnumeration for DefaultDomain { 17 | fn id(&self) -> usize { 18 | 0 19 | } 20 | } 21 | 22 | impl DomainId { 23 | pub(crate) fn new(d: &impl DomainEnumeration) -> DomainId { 24 | DomainId(Some(d.id())) 25 | } 26 | pub(crate) fn index(&self) -> Option { 27 | self.0 28 | } 29 | } 30 | 31 | #[macro_export] 32 | /// Implements DomainEnumeration for an enum. 33 | /// 34 | /// This macro can only be used on primitive enums that implement Copy. 35 | /// The current implementation of the macro unfortunately also requires 36 | /// `DomainEnumeration` to be imported with this exact name. 37 | /// 38 | /// # Example: 39 | // @ START-DOC DOMAIN_MACRO_EXAMPLE 40 | /// ``` 41 | /// #[macro_use] extern crate nuts; 42 | /// use nuts::{domain_enum, DomainEnumeration}; 43 | /// #[derive(Clone, Copy)] 44 | /// enum MyDomain { 45 | /// DomainA, 46 | /// DomainB, 47 | /// } 48 | /// domain_enum!(MyDomain); 49 | /// ``` 50 | // @ END-DOC DOMAIN_MACRO_EXAMPLE 51 | macro_rules! domain_enum { 52 | ( $e:tt ) => { 53 | impl DomainEnumeration for $e { 54 | fn id(&self) -> usize { 55 | *self as usize 56 | } 57 | } 58 | }; 59 | } 60 | -------------------------------------------------------------------------------- /src/nut/iac/managed_state/domain_state.rs: -------------------------------------------------------------------------------- 1 | use core::any::{Any, TypeId}; 2 | use std::collections::{hash_map::Entry, HashMap}; 3 | 4 | use crate::nut::IMPOSSIBLE_ERR_MSG; 5 | 6 | /// Stores passive data that can be accessed in event handlers of multiple activities. 7 | /// 8 | // @ START-DOC DOMAIN 9 | /// A Domain stores arbitrary data for sharing between multiple [Activities](trait.Activity.html). 10 | /// Library users can define the number of domains but each activity can only join one domain. 11 | /// 12 | /// Domains should only be used when data needs to be shared between multiple activities of the same or different types. 13 | /// If data is only used by a single activity, it is usually better to store it in the activity struct itself. 14 | /// 15 | /// In case only one domain is used, you can also consider to use [`DefaultDomain`](struct.DefaultDomain.html) instead of creating your own enum. 16 | /// 17 | /// For now, there is no real benefit from using multiple Domains, other than data isolation. 18 | /// But there are plans for the future that will schedule Activities in different threads, based on their domain. 19 | // @ END-DOC DOMAIN 20 | #[derive(Default)] 21 | pub struct DomainState { 22 | // Indirection to Vec is used here to allow for safe internal mutability without falling back to RefCells. 23 | // (RefCells are uneasy to use from outside AND the runtime hit is larger) 24 | objects: Vec>, 25 | index_map: HashMap, 26 | } 27 | 28 | impl DomainState { 29 | /// Stores a value in the domain. 30 | // @ START-DOC DOMAIN_STORE 31 | /// Only one instance per type id can be stored inside a domain. 32 | /// If an old value of the same type already exists in the domain, it will be overwritten. 33 | // @ END-DOC DOMAIN_STORE 34 | pub fn store(&mut self, obj: T) { 35 | let id = TypeId::of::(); 36 | match self.index_map.entry(id) { 37 | Entry::Occupied(entry) => { 38 | *self.objects[*entry.get()] 39 | .downcast_mut() 40 | .expect(IMPOSSIBLE_ERR_MSG) = obj; 41 | } 42 | Entry::Vacant(entry) => { 43 | entry.insert(self.objects.len()); 44 | self.objects.push(Box::new(obj)); 45 | } 46 | } 47 | } 48 | /// For internal use only. 49 | /// 50 | /// Non-generic variant of store. 51 | /// Used for delayed stores to domains. 52 | /// 53 | /// This variant is slightly less efficient as it will allocate another Box if the value was already in the domain. 54 | pub(crate) fn store_unchecked(&mut self, id: TypeId, obj: Box) { 55 | match self.index_map.entry(id) { 56 | Entry::Occupied(entry) => { 57 | self.objects[*entry.get()] = obj; 58 | } 59 | Entry::Vacant(entry) => { 60 | entry.insert(self.objects.len()); 61 | self.objects.push(obj); 62 | } 63 | } 64 | } 65 | /// Returns a reference to a value of the specified type, if such a value has previously been stored to the domain. 66 | #[allow(clippy::unwrap_used)] 67 | pub fn try_get(&self) -> Option<&T> { 68 | self.index_map 69 | .get(&TypeId::of::()) 70 | .map(|index| self.objects[*index].as_ref().downcast_ref().unwrap()) 71 | } 72 | /// Same as [`try_get`](#try_get) but grants mutable access to the object. 73 | #[allow(clippy::unwrap_used)] 74 | pub fn try_get_mut(&mut self) -> Option<&mut T> { 75 | if let Some(index) = self.index_map.get(&TypeId::of::()) { 76 | Some(self.objects[*index].as_mut().downcast_mut().unwrap()) 77 | } else { 78 | None 79 | } 80 | } 81 | /// Return two mutable references to domain objects 82 | #[allow(clippy::unwrap_used)] 83 | pub fn try_get_2_mut(&mut self) -> (Option<&mut T1>, Option<&mut T2>) { 84 | let type_1: TypeId = TypeId::of::(); 85 | let type_2: TypeId = TypeId::of::(); 86 | assert_ne(type_1, type_2); 87 | let i1 = self.index_map.get(&type_1); 88 | let i2 = self.index_map.get(&type_2); 89 | if i1.is_none() { 90 | return (None, self.try_get_mut()); 91 | } 92 | if i2.is_none() { 93 | return (self.try_get_mut(), None); 94 | } 95 | 96 | let i1 = i1.unwrap(); 97 | let i2 = i2.unwrap(); 98 | 99 | let split = i1.min(i2) + 1; 100 | let (left, right) = self.objects.split_at_mut(split); 101 | 102 | let (t1, t2) = if i1 < i2 { 103 | (&mut left[*i1], &mut right[i2 - split]) 104 | } else { 105 | (&mut right[i1 - split], &mut left[*i2]) 106 | }; 107 | ( 108 | Some(t1.as_mut().downcast_mut().unwrap()), 109 | Some(t2.as_mut().downcast_mut().unwrap()), 110 | ) 111 | } 112 | /// Returns a reference to a value of the specified type, taken from the domain. 113 | /// # Panics 114 | /// Panics if object of that type has not been stored previously. 115 | /// [`try_get()`](#try_get) is usually recommended instead. 116 | #[allow(clippy::unwrap_used)] 117 | pub fn get(&self) -> &T { 118 | self.try_get().expect("Not in domain") 119 | } 120 | /// Returns a mutable reference to a value of the specified type, taken from the domain. 121 | /// # Panics 122 | /// Panics if object of that type has not been stored previously 123 | /// [`try_get_mut()`](#try_get_mut) is usually recommended instead. 124 | #[allow(clippy::unwrap_used)] 125 | pub fn get_mut(&mut self) -> &mut T { 126 | self.try_get_mut().expect("Not in domain") 127 | } 128 | } 129 | // This should really be a const fn so that we get compile-time panic instead of run-time checks. 130 | // But unfortunately, that is currently not possible. 131 | fn assert_ne(t1: TypeId, t2: TypeId) { 132 | if t1 == t2 { 133 | panic!("Cannot get two mutable references of same type from domain") 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/nut/iac/managed_state/domain_store.rs: -------------------------------------------------------------------------------- 1 | use crate::debug::DebugTypeName; 2 | use crate::nut::{Nut, IMPOSSIBLE_ERR_MSG}; 3 | use crate::DomainId; 4 | use core::any::{Any, TypeId}; 5 | 6 | pub(crate) struct DomainStoreData { 7 | domain: DomainId, 8 | id: TypeId, 9 | data: Box, 10 | #[allow(dead_code)] 11 | type_name: DebugTypeName, 12 | } 13 | impl Nut { 14 | pub fn exec_domain_store(&self, d: DomainStoreData) { 15 | self.managed_state 16 | .try_borrow_mut() 17 | .expect(IMPOSSIBLE_ERR_MSG) 18 | .get_mut(d.domain) 19 | .expect("Domain ID invalid") 20 | .store_unchecked(d.id, d.data); 21 | } 22 | } 23 | 24 | impl DomainStoreData { 25 | pub fn new(domain: DomainId, data: DATA) -> Self { 26 | Self { 27 | domain, 28 | id: TypeId::of::(), 29 | data: Box::new(data), 30 | type_name: DebugTypeName::new::(), 31 | } 32 | } 33 | } 34 | 35 | #[cfg(debug_assertions)] 36 | impl std::fmt::Debug for DomainStoreData { 37 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 38 | write!(f, "Storing {:?} to the domain", self.type_name) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/nut/iac/publish.rs: -------------------------------------------------------------------------------- 1 | pub(crate) use broadcast::BroadcastInfo; 2 | 3 | mod broadcast; 4 | mod response; 5 | pub(crate) use response::ResponseTracker; 6 | pub(crate) use response::Slot as ResponseSlot; 7 | 8 | use crate::nut::Nut; 9 | use crate::*; 10 | use core::any::Any; 11 | 12 | use self::response::NutsResponse; 13 | 14 | impl Nut { 15 | pub(crate) fn broadcast(&self, broadcast: BroadcastInfo) { 16 | self.deferred_events.push(broadcast.into()); 17 | self.catch_up_deferred_to_quiescence(); 18 | } 19 | pub(crate) fn publish_and_await(&self, msg: MSG) -> NutsResponse { 20 | let broadcast = BroadcastInfo::global(msg, Topic::public_message::()); 21 | let ticket = Nut::with_response_tracker_mut(|rt| rt.allocate()); 22 | let future = NutsResponse::new(&ticket); 23 | self.deferred_events 24 | .push(nut::exec::Deferred::BroadcastAwaitingResponse( 25 | broadcast, ticket, 26 | )); 27 | self.catch_up_deferred_to_quiescence(); 28 | future 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/nut/iac/publish/broadcast.rs: -------------------------------------------------------------------------------- 1 | use crate::debug::DebugTypeName; 2 | use crate::nut::{iac::subscription::Subscription, Nut}; 3 | use crate::*; 4 | use core::any::{Any, TypeId}; 5 | use std::cell::RefMut; 6 | 7 | pub(crate) struct BroadcastInfo { 8 | address: BroadcastAddress, 9 | msg: Box, 10 | topic: Topic, 11 | #[allow(dead_code)] 12 | type_name: DebugTypeName, 13 | } 14 | 15 | enum BroadcastAddress { 16 | Local(UncheckedActivityId), 17 | LocalByType(TypeId), 18 | Global, 19 | } 20 | 21 | impl BroadcastInfo { 22 | pub(crate) fn global(msg: MSG, topic: Topic) -> Self { 23 | BroadcastInfo { 24 | address: BroadcastAddress::Global, 25 | msg: Box::new(msg), 26 | topic, 27 | type_name: DebugTypeName::new::(), 28 | } 29 | } 30 | pub(crate) fn local(msg: MSG, id: UncheckedActivityId, topic: Topic) -> Self { 31 | BroadcastInfo { 32 | address: BroadcastAddress::Local(id), 33 | msg: Box::new(msg), 34 | topic, 35 | type_name: DebugTypeName::new::(), 36 | } 37 | } 38 | pub(crate) fn local_by_type(msg: MSG, topic: Topic) -> Self { 39 | BroadcastInfo { 40 | address: BroadcastAddress::LocalByType(TypeId::of::()), 41 | msg: Box::new(msg), 42 | topic, 43 | type_name: DebugTypeName::new::(), 44 | } 45 | } 46 | } 47 | 48 | impl Nut { 49 | /// only access after locking with executing flag 50 | pub(crate) fn unchecked_broadcast(&self, broadcast: BroadcastInfo) { 51 | let mut managed_state = self.managed_state.borrow_mut(); 52 | managed_state.set_broadcast(broadcast.msg); 53 | if let Some(handlers) = self.subscriptions.get().get(&broadcast.topic) { 54 | match self.receiver_id(&broadcast.address) { 55 | None => { 56 | for sub in handlers.shared_subscriptions() { 57 | self.call_subscriber(sub, &mut managed_state); 58 | } 59 | } 60 | Some(id) => { 61 | if broadcast.topic.unqiue_per_activity() { 62 | if let Some(sub) = handlers.private_subscription(id) { 63 | self.call_subscriber(sub, &mut managed_state); 64 | } 65 | } else { 66 | for sub in handlers.shared_subscriptions_of_single_activity(id) { 67 | self.call_subscriber(sub, &mut managed_state); 68 | } 69 | } 70 | } 71 | } 72 | #[cfg(debug_assertions)] 73 | self.active_activity_name.set(None); 74 | } 75 | managed_state.clear_broadcast(); 76 | } 77 | fn call_subscriber(&self, sub: &Subscription, managed_state: &mut RefMut) { 78 | #[cfg(debug_assertions)] 79 | self.active_activity_name.set(Some(sub.type_name)); 80 | let f = &sub.handler; 81 | f(&mut self.activities.borrow_mut(), managed_state); 82 | } 83 | fn receiver_id(&self, address: &BroadcastAddress) -> Option { 84 | match address { 85 | BroadcastAddress::Global => None, 86 | BroadcastAddress::Local(id) => Some(*id), 87 | BroadcastAddress::LocalByType(t) => self.activities.borrow().id_lookup(*t), 88 | } 89 | } 90 | } 91 | 92 | #[cfg(debug_assertions)] 93 | impl std::fmt::Debug for BroadcastInfo { 94 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 95 | match self.address { 96 | BroadcastAddress::Global => write!(f, "published message of type {:?}", self.type_name), 97 | BroadcastAddress::Local(_) => write!(f, "{:?} event", self.topic), 98 | BroadcastAddress::LocalByType(_) => { 99 | write!(f, "message of type {:?} (sent privately)", self.type_name) 100 | } 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/nut/iac/publish/response.rs: -------------------------------------------------------------------------------- 1 | use std::{future::Future, task::Poll}; 2 | 3 | use crate::nut::Nut; 4 | 5 | #[derive(Default)] 6 | pub(crate) struct ResponseTracker { 7 | slots: Vec, 8 | } 9 | 10 | enum SlotState { 11 | Available, 12 | Occupied, 13 | Done, 14 | } 15 | 16 | pub(crate) struct Slot(usize); 17 | 18 | #[allow(clippy::single_match)] 19 | impl ResponseTracker { 20 | pub fn allocate(&mut self) -> Slot { 21 | for (i, slot) in self.slots.iter_mut().enumerate() { 22 | match slot { 23 | SlotState::Available => { 24 | *slot = SlotState::Occupied; 25 | return Slot(i); 26 | } 27 | _ => {} 28 | } 29 | } 30 | let i = self.slots.len(); 31 | self.slots.push(SlotState::Occupied); 32 | Slot(i) 33 | } 34 | pub fn done(&mut self, slot: &Slot) { 35 | self.slots[slot.0] = SlotState::Done; 36 | } 37 | fn free(&mut self, index: usize) { 38 | self.slots[index] = SlotState::Available; 39 | } 40 | } 41 | 42 | pub struct NutsResponse { 43 | index: usize, 44 | } 45 | 46 | impl NutsResponse { 47 | pub(crate) fn new(slot: &Slot) -> Self { 48 | Self { index: slot.0 } 49 | } 50 | } 51 | 52 | impl Future for NutsResponse { 53 | type Output = (); 54 | 55 | fn poll( 56 | self: std::pin::Pin<&mut Self>, 57 | _cx: &mut std::task::Context<'_>, 58 | ) -> Poll { 59 | Nut::with_response_tracker_mut(|response_tracker| { 60 | match response_tracker.slots[self.index] { 61 | SlotState::Available => panic!("Corrupted futures State"), 62 | SlotState::Occupied => Poll::Pending, 63 | SlotState::Done => { 64 | response_tracker.free(self.index); 65 | Poll::Ready(()) 66 | } 67 | } 68 | }) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/nut/iac/subscription.rs: -------------------------------------------------------------------------------- 1 | use super::{managed_state::ManagedState, topic::Topic}; 2 | use crate::{ 3 | debug::DebugTypeName, 4 | nut::{exec::Deferred, Handler, Nut, IMPOSSIBLE_ERR_MSG}, 5 | ActivityId, UncheckedActivityId, 6 | }; 7 | use core::cell::Ref; 8 | use std::{ 9 | any::Any, 10 | cell::RefCell, 11 | collections::HashMap, 12 | ops::{Index, IndexMut}, 13 | }; 14 | 15 | #[derive(Default)] 16 | pub(crate) struct Subscriptions { 17 | subscriptions: RefCell>, 18 | } 19 | 20 | /// Handlers stored per Activity 21 | #[derive(Default)] 22 | pub(crate) struct SubscriptionContainer { 23 | data: HashMap, 24 | } 25 | 26 | /// Handlers per type per activity 27 | #[derive(Default)] 28 | pub(crate) struct ActivityTopicSubscriptions { 29 | shared: Vec, 30 | private: Option, 31 | } 32 | 33 | pub(crate) struct Subscription { 34 | pub(crate) handler: Handler, 35 | #[allow(dead_code)] 36 | pub(crate) type_name: DebugTypeName, 37 | } 38 | 39 | pub(crate) enum OnDelete { 40 | None, 41 | Simple(Box)>), 42 | WithDomain(Box, &mut ManagedState)>), 43 | } 44 | 45 | impl Nut { 46 | pub(crate) fn push_closure( 47 | &self, 48 | topic: Topic, 49 | id: ActivityId, 50 | closure: Handler, 51 | ) { 52 | let type_name = DebugTypeName::new::(); 53 | if self.quiescent() { 54 | self.subscriptions 55 | .force_push_closure(topic, id, closure, type_name); 56 | } else { 57 | let sub = NewSubscription::new(topic, id, closure, type_name); 58 | self.deferred_events.push(Deferred::Subscription(sub)); 59 | } 60 | } 61 | } 62 | 63 | impl Subscriptions { 64 | pub(crate) fn exec_new_subscription(&self, sub: NewSubscription) { 65 | self.force_push_closure(sub.topic, sub.id, sub.closure, sub.type_name); 66 | } 67 | fn force_push_closure( 68 | &self, 69 | topic: Topic, 70 | id: impl Into, 71 | handler: Handler, 72 | type_name: DebugTypeName, 73 | ) { 74 | let id = id.into(); 75 | let private = topic.unqiue_per_activity(); 76 | let subs = &mut self 77 | .subscriptions 78 | .try_borrow_mut() 79 | .expect(IMPOSSIBLE_ERR_MSG); 80 | let subs_per_activity = &mut subs.entry(topic).or_insert_with(Default::default)[id]; 81 | 82 | if private { 83 | subs_per_activity.private = Some(Subscription { handler, type_name }); 84 | } else { 85 | subs_per_activity 86 | .shared 87 | .push(Subscription { handler, type_name }); 88 | } 89 | } 90 | pub(crate) fn get(&self) -> Ref> { 91 | self.subscriptions.borrow() 92 | } 93 | } 94 | 95 | impl SubscriptionContainer { 96 | pub fn shared_subscriptions(&self) -> impl Iterator { 97 | self.data.values().flat_map(|f| f.shared.iter()) 98 | } 99 | pub fn shared_subscriptions_of_single_activity( 100 | &self, 101 | id: UncheckedActivityId, 102 | ) -> impl Iterator { 103 | self.data 104 | .get(&id.index) 105 | .into_iter() 106 | .flat_map(|f| f.shared.iter()) 107 | } 108 | pub fn private_subscription(&self, id: UncheckedActivityId) -> Option<&Subscription> { 109 | self.data 110 | .get(&id.index) 111 | .map(|f| f.private.as_ref()) 112 | .flatten() 113 | } 114 | } 115 | impl Index for SubscriptionContainer { 116 | type Output = ActivityTopicSubscriptions; 117 | fn index(&self, id: UncheckedActivityId) -> &Self::Output { 118 | &self.data[&id.index] 119 | } 120 | } 121 | impl IndexMut for SubscriptionContainer { 122 | fn index_mut(&mut self, id: UncheckedActivityId) -> &mut Self::Output { 123 | self.data.entry(id.index).or_insert_with(Default::default) 124 | } 125 | } 126 | 127 | pub(crate) struct NewSubscription { 128 | topic: Topic, 129 | id: UncheckedActivityId, 130 | closure: Handler, 131 | type_name: DebugTypeName, 132 | } 133 | 134 | impl NewSubscription { 135 | fn new( 136 | topic: Topic, 137 | id: impl Into, 138 | closure: Handler, 139 | type_name: DebugTypeName, 140 | ) -> Self { 141 | Self { 142 | topic, 143 | id: id.into(), 144 | closure, 145 | type_name, 146 | } 147 | } 148 | } 149 | 150 | #[cfg(debug_assertions)] 151 | impl std::fmt::Debug for NewSubscription { 152 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 153 | write!( 154 | f, 155 | "Adding new subscription to activity of type {:?}", 156 | self.type_name 157 | ) 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/nut/iac/topic.rs: -------------------------------------------------------------------------------- 1 | use core::any::{Any, TypeId}; 2 | 3 | /// A topic for messages that can be published and subscribed to 4 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 5 | pub(crate) enum Topic { 6 | /// Topic for a builtin event 7 | BuiltinEvent(BuiltinEvent), 8 | /// Topic for a message type, where type is a Rust type (core::any::TypeId). Many receivers can coexists for each published message. 9 | PublicMessage(TypeId), 10 | /// Topic for a message type, where type is a Rust type (core::any::TypeId). Only one receiver can exist per activity and each message must be sent to exactly one activity. 11 | PrivateMessage(TypeId), 12 | } 13 | 14 | /// Builtin events are messages without payload that are used internally. 15 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 16 | pub(crate) enum BuiltinEvent { 17 | /// On status change to active (not called if started as active) 18 | Enter, 19 | /// On status change to inactive / deleted 20 | Leave, 21 | } 22 | 23 | impl Topic { 24 | pub(crate) fn enter() -> Self { 25 | Self::BuiltinEvent(BuiltinEvent::Enter) 26 | } 27 | pub(crate) fn leave() -> Self { 28 | Self::BuiltinEvent(BuiltinEvent::Leave) 29 | } 30 | pub(crate) fn public_message() -> Self { 31 | Self::PublicMessage(TypeId::of::()) 32 | } 33 | pub(crate) fn private_message() -> Self { 34 | Self::PrivateMessage(TypeId::of::()) 35 | } 36 | pub(crate) fn unqiue_per_activity(&self) -> bool { 37 | match self { 38 | Self::BuiltinEvent(_) | Self::PublicMessage(_) => false, 39 | Self::PrivateMessage(_) => true, 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/test.rs: -------------------------------------------------------------------------------- 1 | mod base_tests; 2 | mod domain_tests; 3 | mod inchoate_tests; 4 | mod lifecycle_tests; 5 | 6 | use crate::*; 7 | use std::cell::Cell; 8 | use std::rc::Rc; 9 | 10 | #[derive(Clone)] 11 | struct TestActivity { 12 | counter: Rc>, 13 | } 14 | 15 | impl TestActivity { 16 | fn new() -> Self { 17 | let shared_counter = Rc::new(Cell::new(0)); 18 | Self { 19 | counter: shared_counter, 20 | } 21 | } 22 | fn shared_counter_ref(&self) -> Rc> { 23 | self.counter.clone() 24 | } 25 | fn inc(&self, add: u32) { 26 | let i = self.counter.get(); 27 | self.counter.as_ref().set(i + add) 28 | } 29 | } 30 | 31 | #[derive(Clone, Copy)] 32 | enum TestDomains { 33 | DomainA, 34 | _DomainB, 35 | } 36 | domain_enum!(TestDomains); 37 | 38 | struct TestUpdateMsg; 39 | struct TestForInt(usize); 40 | struct TestMessage(u32); 41 | struct TestMessageNoClone; 42 | -------------------------------------------------------------------------------- /src/test/base_tests.rs: -------------------------------------------------------------------------------- 1 | //! Test suite for activity creation and subscription registration. 2 | 3 | use super::*; 4 | #[test] 5 | // A simple sanity test for registering an activity. 6 | // The registered function should crucially only be called once. 7 | // The test should be considered in combination with `closure_registration_negative` 8 | fn closure_registration() { 9 | let a = TestActivity::new(); 10 | let counter = a.shared_counter_ref(); 11 | let id = crate::new_activity(a); 12 | id.subscribe(|activity: &mut TestActivity, _: &TestUpdateMsg| { 13 | activity.inc(1); 14 | }); 15 | assert_eq!(counter.get(), 0, "Closure called before update call"); 16 | crate::publish(TestUpdateMsg); 17 | assert_eq!(counter.get(), 1); 18 | crate::publish(TestUpdateMsg); 19 | assert_eq!(counter.get(), 2); 20 | } 21 | 22 | #[test] 23 | fn closure_registration_no_activity() { 24 | use std::sync::atomic::{AtomicBool, Ordering}; 25 | 26 | let executed: Rc = Rc::new(AtomicBool::new(false)); 27 | let cloned_executed = executed.clone(); 28 | crate::subscribe(move |_: &TestUpdateMsg| { 29 | cloned_executed.store(true, Ordering::SeqCst); 30 | }); 31 | assert!( 32 | !executed.load(Ordering::SeqCst), 33 | "Closure called before update call" 34 | ); 35 | crate::publish(TestUpdateMsg); 36 | assert!(executed.load(Ordering::SeqCst)); 37 | crate::publish(TestUpdateMsg); 38 | assert!(executed.load(Ordering::SeqCst)); 39 | } 40 | 41 | #[test] 42 | fn domained_activity() { 43 | let a = TestActivity::new(); 44 | let d = TestDomains::DomainA; 45 | crate::store_to_domain(&d, 7usize); 46 | let id = crate::new_domained_activity(a, &d); 47 | id.subscribe_domained(|_activity, domain, _msg: &TestUpdateMsg| { 48 | let x: usize = *domain.get(); 49 | assert_eq!(7, x); 50 | }); 51 | crate::publish(TestUpdateMsg); 52 | } 53 | 54 | #[test] 55 | fn message_passing() { 56 | // Set up activity that increases a counter by the value specified in messages of type TestMessage 57 | let a = TestActivity::new(); 58 | let counter = a.shared_counter_ref(); 59 | let id = crate::new_activity(a); 60 | id.subscribe(|activity, msg: &TestMessage| { 61 | activity.inc(msg.0); 62 | }); 63 | 64 | // Send different values and check that subscribed code has been called 65 | crate::publish(TestMessage(13)); 66 | assert_eq!(counter.get(), 13); 67 | 68 | crate::publish(TestMessage(13)); 69 | assert_eq!(counter.get(), 26); 70 | 71 | crate::publish(TestMessage(100)); 72 | assert_eq!(counter.get(), 126); 73 | } 74 | 75 | #[test] 76 | fn owned_message() { 77 | let a = TestActivity::new(); 78 | let counter = a.shared_counter_ref(); 79 | let id = crate::new_activity(a); 80 | id.private_channel(|activity, _msg: TestMessageNoClone| { 81 | activity.inc(1); 82 | }); 83 | crate::send_to::(TestMessageNoClone); 84 | assert_eq!(1, counter.get()); // Make sure subscription has been called 85 | } 86 | #[test] 87 | fn owned_domained_message() { 88 | let a = TestActivity::new(); 89 | let counter = a.shared_counter_ref(); 90 | let d = TestDomains::DomainA; 91 | crate::store_to_domain(&d, 7usize); 92 | let id = crate::new_domained_activity(a, &d); 93 | id.private_domained_channel(|activity, domain, _msg: TestMessageNoClone| { 94 | let x: usize = *domain.get(); 95 | assert_eq!(7, x); 96 | activity.inc(1); 97 | }); 98 | crate::send_to::(TestMessageNoClone); 99 | assert_eq!(1, counter.get()); // Make sure subscription has been called 100 | } 101 | 102 | #[test] 103 | fn publish_inside_publish() { 104 | const LAYERS: u32 = 5; 105 | 106 | let a = TestActivity::new(); 107 | let counter = a.shared_counter_ref(); 108 | let id = crate::new_activity(a); 109 | id.subscribe(|activity, _msg: &TestUpdateMsg| { 110 | if activity.counter.get() < LAYERS { 111 | activity.inc(1); 112 | crate::publish(TestUpdateMsg); 113 | } 114 | }); 115 | crate::publish(TestUpdateMsg); 116 | 117 | assert_eq!(LAYERS, counter.get()); 118 | } 119 | 120 | #[test] 121 | fn private_message() { 122 | let a = TestActivity::new(); 123 | let counter = a.shared_counter_ref(); 124 | let id = crate::new_activity(a); 125 | id.private_channel(|activity, _msg: TestMessageNoClone| { 126 | activity.inc(1); 127 | }); 128 | crate::publish(TestMessageNoClone); 129 | assert_eq!(0, counter.get()); // Make sure subscription has not been called, yet 130 | crate::send_to::(TestMessageNoClone); 131 | assert_eq!(1, counter.get()); // Make sure subscription has been called 132 | } 133 | 134 | #[test] 135 | fn private_message_by_id() { 136 | let a = TestActivity::new(); 137 | let counter = a.shared_counter_ref(); 138 | let id = crate::new_activity(a); 139 | id.private_channel(|activity, _msg: TestMessageNoClone| { 140 | activity.inc(1); 141 | }); 142 | crate::publish(TestMessageNoClone); 143 | assert_eq!(0, counter.get()); // Make sure subscription has not been called, yet 144 | id.private_message(TestMessageNoClone); 145 | assert_eq!(1, counter.get()); // Make sure subscription has been called 146 | } 147 | 148 | #[test] 149 | fn multi_subscribe_private_message() { 150 | let a = TestActivity::new(); 151 | let counter = a.shared_counter_ref(); 152 | let id = crate::new_activity(a); 153 | id.private_channel(|activity, _msg: TestMessageNoClone| { 154 | activity.inc(10); 155 | }); 156 | id.private_channel(|activity, _msg: TestMessageNoClone| { 157 | activity.inc(1); 158 | }); 159 | crate::publish(TestMessageNoClone); 160 | assert_eq!(0, counter.get()); // Make sure subscription has not been called, yet 161 | crate::send_to::(TestMessageNoClone); 162 | assert_eq!(1, counter.get()); // Make sure second subscription has been called exactly once 163 | } 164 | 165 | #[test] 166 | fn multi_subscriber_private_message() { 167 | let a = TestActivity::new(); 168 | let b = (TestActivity::new(),); 169 | let counter = a.shared_counter_ref(); 170 | let aid = crate::new_activity(a); 171 | let bid = crate::new_activity(b); 172 | aid.private_channel(|activity, _msg: TestMessageNoClone| { 173 | activity.inc(1); 174 | }); 175 | bid.private_channel(|activity, _msg: TestMessageNoClone| { 176 | activity.0.inc(10); 177 | }); 178 | crate::publish(TestMessageNoClone); 179 | assert_eq!(0, counter.get()); // Make sure subscription has not been called, yet 180 | crate::send_to::(TestMessageNoClone); 181 | assert_eq!(1, counter.get()); // Make sure subscription of correct type has been called exactly once 182 | } 183 | -------------------------------------------------------------------------------- /src/test/domain_tests.rs: -------------------------------------------------------------------------------- 1 | //! Test suite for domain usage. 2 | use super::*; 3 | 4 | #[test] 5 | fn store_to_domain_inside_activity() { 6 | let a = TestActivity::new(); 7 | let d = TestDomains::DomainA; 8 | crate::store_to_domain(&d, 7usize); 9 | let id = crate::new_domained_activity(a, &d); 10 | id.subscribe_domained(|_activity, domain, msg: &TestForInt| { 11 | let x: usize = *domain.get(); 12 | assert_eq!(msg.0, x); 13 | }); 14 | id.subscribe_domained(|_activity, domain, _msg: &TestUpdateMsg| { 15 | let d = TestDomains::DomainA; 16 | // check we can read domain values 17 | let x: usize = *domain.get(); 18 | assert_eq!(7usize, x); 19 | // check we can write domain values 20 | *domain.get_mut() = 8usize; 21 | let x: usize = *domain.get(); 22 | assert_eq!(8usize, x); 23 | // Check we can store to arbitrary domain (which will have to be deferred in this example) 24 | crate::store_to_domain(&d, 9usize); 25 | 26 | // Update should no be visible, yet, as we are locking the domain 27 | let x: usize = *domain.get(); 28 | assert_eq!(8, x); 29 | }); 30 | // Check domain value before 31 | crate::publish(TestForInt(7)); 32 | // Update value from inside the subscriber 33 | crate::publish(TestUpdateMsg); 34 | // Check update has been completed 35 | crate::publish(TestForInt(9)); 36 | } 37 | -------------------------------------------------------------------------------- /src/test/inchoate_tests.rs: -------------------------------------------------------------------------------- 1 | //! Testing creation and managing of activities while a broadcast is inflight. 2 | use super::*; 3 | 4 | struct Main; 5 | 6 | #[test] 7 | fn create_inchoate_activity() { 8 | let main = crate::new_activity(()); 9 | let a = TestActivity::new(); 10 | let counter = a.shared_counter_ref(); 11 | let aid_slot: Rc>>> = Default::default(); 12 | let aid_slot_clone = aid_slot.clone(); 13 | main.subscribe(move |_, _: &Main| { 14 | let aid = crate::new_activity(a.clone()); 15 | aid_slot.set(Some(aid)); 16 | }); 17 | 18 | crate::publish(Main); 19 | 20 | let id = aid_slot_clone.get().unwrap(); 21 | id.subscribe(|activity: &mut TestActivity, _: &TestUpdateMsg| { 22 | activity.inc(1); 23 | }); 24 | assert_eq!(counter.get(), 0, "Closure called before update call"); 25 | crate::publish(TestUpdateMsg); 26 | assert_eq!(counter.get(), 1); 27 | crate::publish(TestUpdateMsg); 28 | assert_eq!(counter.get(), 2); 29 | } 30 | 31 | #[test] 32 | fn create_inchoate_activity_and_subscribe() { 33 | let main = crate::new_activity(()); 34 | let a = TestActivity::new(); 35 | let counter = a.shared_counter_ref(); 36 | main.subscribe(move |_, _: &Main| { 37 | let id = crate::new_activity(a.clone()); 38 | id.subscribe(|activity: &mut TestActivity, _: &TestUpdateMsg| { 39 | activity.inc(1); 40 | }); 41 | }); 42 | 43 | crate::publish(Main); 44 | 45 | assert_eq!(counter.get(), 0, "Closure called before update call"); 46 | crate::publish(TestUpdateMsg); 47 | assert_eq!(counter.get(), 1); 48 | crate::publish(TestUpdateMsg); 49 | assert_eq!(counter.get(), 2); 50 | } 51 | 52 | #[test] 53 | fn create_inchoate_domained_activity_and_subscribe() { 54 | let main = crate::new_activity(()); 55 | let a = TestActivity::new(); 56 | let counter = a.shared_counter_ref(); 57 | let d = TestDomains::DomainA; 58 | crate::store_to_domain(&d, 7usize); 59 | 60 | main.subscribe(move |_, _: &Main| { 61 | let id = crate::new_domained_activity(a.clone(), &d); 62 | id.subscribe_domained(|activity: &mut TestActivity, domain, _: &TestUpdateMsg| { 63 | let x: usize = *domain.get(); 64 | assert_eq!(7, x); 65 | activity.inc(1); 66 | }); 67 | }); 68 | 69 | crate::publish(Main); 70 | 71 | assert_eq!(counter.get(), 0, "Closure called before update call"); 72 | crate::publish(TestUpdateMsg); 73 | assert_eq!(counter.get(), 1); 74 | crate::publish(TestUpdateMsg); 75 | assert_eq!(counter.get(), 2); 76 | } 77 | 78 | #[test] 79 | fn create_inchoate_domained_activity_and_subscribe_and_publish() { 80 | let main = crate::new_activity(()); 81 | let a = TestActivity::new(); 82 | let b = (TestActivity::new(),); 83 | let counter = a.shared_counter_ref(); 84 | let d = TestDomains::DomainA; 85 | crate::store_to_domain(&d, 7usize); 86 | 87 | let bid = crate::new_domained_activity(b, &d); 88 | bid.subscribe_domained(|_activity, domain, msg: &TestForInt| { 89 | let x: usize = *domain.get(); 90 | assert_eq!(msg.0, x); 91 | }); 92 | 93 | main.subscribe(move |_, _: &Main| { 94 | let id = crate::new_domained_activity(a.clone(), &d); 95 | id.subscribe_domained(|activity: &mut TestActivity, domain, _: &TestUpdateMsg| { 96 | let x: usize = *domain.get(); 97 | assert_eq!(7, x); 98 | activity.inc(1); 99 | }); 100 | crate::publish(TestForInt(7)); 101 | }); 102 | 103 | crate::publish(Main); 104 | 105 | assert_eq!(counter.get(), 0, "Closure called before update call"); 106 | crate::publish(TestUpdateMsg); 107 | assert_eq!(counter.get(), 1); 108 | crate::publish(TestUpdateMsg); 109 | assert_eq!(counter.get(), 2); 110 | } 111 | 112 | #[test] 113 | fn create_inchoate_domained_activity_and_subscribe_and_publish_to_inchoate_activity() { 114 | let main = crate::new_activity(()); 115 | let a = TestActivity::new(); 116 | let counter = a.shared_counter_ref(); 117 | let d = TestDomains::DomainA; 118 | crate::store_to_domain(&d, 7usize); 119 | 120 | main.subscribe(move |_, _: &Main| { 121 | let id = crate::new_domained_activity(a.clone(), &d); 122 | id.subscribe_domained(|_activity, domain, msg: &TestForInt| { 123 | let x: usize = *domain.get(); 124 | assert_eq!(msg.0, x); 125 | }); 126 | id.subscribe_domained(|activity: &mut TestActivity, domain, _: &TestUpdateMsg| { 127 | let x: usize = *domain.get(); 128 | assert_eq!(7, x); 129 | activity.inc(1); 130 | }); 131 | crate::publish(TestForInt(7)); 132 | }); 133 | 134 | crate::publish(Main); 135 | 136 | assert_eq!(counter.get(), 0, "Closure called before update call"); 137 | crate::publish(TestUpdateMsg); 138 | assert_eq!(counter.get(), 1); 139 | crate::publish(TestUpdateMsg); 140 | assert_eq!(counter.get(), 2); 141 | } 142 | 143 | #[test] 144 | fn queue_message_and_add_inchoate_subscriber() { 145 | let main = crate::new_activity(()); 146 | let a = TestActivity::new(); 147 | let d = TestDomains::DomainA; 148 | crate::store_to_domain(&d, 7usize); 149 | 150 | main.subscribe(move |_, _: &Main| { 151 | crate::publish(TestForInt(7)); 152 | let id = crate::new_activity(a.clone()); 153 | id.subscribe_domained(|_activity, domain, msg: &TestForInt| { 154 | let x: usize = *domain.get(); 155 | assert_eq!(msg.0, x); 156 | }); 157 | id.subscribe_domained(|activity: &mut TestActivity, domain, _: &TestUpdateMsg| { 158 | let x: usize = *domain.get(); 159 | assert_eq!(7, x); 160 | activity.inc(1); 161 | }); 162 | }); 163 | 164 | crate::publish(Main); 165 | } 166 | 167 | #[test] 168 | // Simple test to make sure nothing panics. More detailed tests afterwards. 169 | fn delete_inchoate_activity() { 170 | let main = crate::new_activity(()); 171 | let a = TestActivity::new(); 172 | let d = TestDomains::DomainA; 173 | crate::store_to_domain(&d, 7u32); 174 | 175 | main.subscribe(move |_, _: &Main| { 176 | let id = crate::new_activity(a.clone()); 177 | id.set_status(LifecycleStatus::Deleted); 178 | }); 179 | crate::publish(Main); 180 | } 181 | 182 | #[test] 183 | fn inchoate_ondelete() { 184 | let main = crate::new_activity(()); 185 | let a = TestActivity::new(); 186 | let counter = a.shared_counter_ref(); 187 | let d = TestDomains::DomainA; 188 | crate::store_to_domain(&d, 7u32); 189 | 190 | main.subscribe(move |_, _: &Main| { 191 | crate::publish(TestForInt(7)); 192 | let id = crate::new_activity(a.clone()); 193 | id.on_delete(|a| a.inc(10)); 194 | id.set_status(LifecycleStatus::Deleted); 195 | }); 196 | assert_eq!(counter.get(), 0); 197 | crate::publish(Main); 198 | assert_eq!(counter.get(), 10); 199 | } 200 | 201 | #[test] 202 | fn delete_inchoate_activity_with_subscriber_and_on_delete() { 203 | let main = crate::new_activity(()); 204 | let a = TestActivity::new(); 205 | let counter = a.shared_counter_ref(); 206 | let d = TestDomains::DomainA; 207 | crate::store_to_domain(&d, 7u32); 208 | 209 | main.subscribe(move |_, _: &Main| { 210 | crate::publish(TestForInt(7)); 211 | let id = crate::new_domained_activity(a.clone(), &d); 212 | id.subscribe_domained(|_activity, _domain, _msg: &TestMessage| { 213 | panic!("Activity should be deleted by now, why is the subscriber called?") 214 | }); 215 | id.on_delete_domained(|a, domain| { 216 | let number: u32 = *domain.get(); 217 | a.inc(number + 5); 218 | }); 219 | id.set_status(LifecycleStatus::Deleted); 220 | }); 221 | 222 | assert_eq!(counter.get(), 0); 223 | crate::publish(Main); 224 | assert_eq!(counter.get(), 12); 225 | crate::publish(TestMessage(0)); 226 | } 227 | 228 | #[test] 229 | fn on_enter_and_leave_and_delete_for_inchoate_activity() { 230 | let main = crate::new_activity(()); 231 | let a = TestActivity::new(); 232 | let counter = a.shared_counter_ref(); 233 | let d = TestDomains::DomainA; 234 | crate::store_to_domain(&d, 7usize); 235 | 236 | let aid_slot: Rc>>> = Default::default(); 237 | let aid_slot_clone = aid_slot.clone(); 238 | 239 | main.subscribe(move |_, _: &Main| { 240 | let id = crate::new_domained_activity(a.clone(), &d); 241 | id.on_enter_domained(|activity: &mut TestActivity, domain| { 242 | let x: usize = *domain.get(); 243 | assert_eq!(7, x); 244 | activity.inc(1); 245 | }); 246 | id.on_leave_domained(|activity: &mut TestActivity, domain| { 247 | let x: usize = *domain.get(); 248 | assert_eq!(7, x); 249 | activity.inc(10); 250 | }); 251 | id.on_delete_domained(|activity: TestActivity, domain| { 252 | let x: usize = *domain.get(); 253 | assert_eq!(7, x); 254 | activity.inc(100); 255 | }); 256 | id.set_status(LifecycleStatus::Inactive); 257 | id.set_status(LifecycleStatus::Active); 258 | aid_slot.set(Some(id)); 259 | }); 260 | 261 | assert_eq!(counter.get(), 0); 262 | crate::publish(Main); 263 | assert_eq!(counter.get(), 11); 264 | 265 | let aid = aid_slot_clone.get().unwrap(); 266 | aid.set_status(LifecycleStatus::Inactive); 267 | assert_eq!(counter.get(), 21,); 268 | 269 | aid.set_status(LifecycleStatus::Active); 270 | assert_eq!(counter.get(), 22,); 271 | 272 | aid.set_status(LifecycleStatus::Deleted); 273 | assert_eq!(counter.get(), 132,); 274 | } 275 | 276 | #[test] 277 | fn create_inchoate_domained_activity_and_subscribe_and_publish_privately() { 278 | let main = crate::new_activity(()); 279 | let a = TestActivity::new(); 280 | let b = (TestActivity::new(),); 281 | let counter = a.shared_counter_ref(); 282 | let d = TestDomains::DomainA; 283 | crate::store_to_domain(&d, 7usize); 284 | 285 | let bid = crate::new_domained_activity(b, &d); 286 | bid.private_domained_channel(|_activity, domain, msg: &TestForInt| { 287 | let x: usize = *domain.get(); 288 | assert_eq!(msg.0, x); 289 | }); 290 | 291 | main.private_channel(move |_, _: Main| { 292 | let id = crate::new_domained_activity(a.clone(), &d); 293 | id.private_domained_channel(|activity: &mut TestActivity, domain, _: TestUpdateMsg| { 294 | let x: usize = *domain.get(); 295 | assert_eq!(7, x); 296 | activity.inc(1); 297 | }); 298 | crate::send_to::<(TestActivity,), _>(TestForInt(7)); 299 | }); 300 | 301 | crate::send_to::<(), _>(Main); 302 | 303 | assert_eq!(counter.get(), 0, "Closure called before update call"); 304 | crate::send_to::(TestUpdateMsg); 305 | assert_eq!(counter.get(), 1); 306 | crate::send_to::(TestUpdateMsg); 307 | assert_eq!(counter.get(), 2); 308 | } 309 | 310 | #[test] 311 | fn create_new_after_delete() { 312 | let main = crate::new_activity(()); 313 | 314 | main.private_channel(move |_, _: Main| { 315 | let a = TestActivity::new(); 316 | let id_a = crate::new_activity(a); 317 | id_a.set_status(LifecycleStatus::Deleted); 318 | let num_a: UncheckedActivityId = id_a.into(); 319 | 320 | let b = (TestActivity::new(),); 321 | let id_b = crate::new_activity(b); 322 | let num_b: UncheckedActivityId = id_b.into(); 323 | 324 | assert_ne!(num_a, num_b); 325 | }); 326 | crate::send_to::<(), _>(Main); 327 | } 328 | 329 | #[test] 330 | /// Create a (normal) activity A and register on_delete 331 | /// Delete A, in A.on_delete: 332 | /// Create (inchoate) activity B 333 | /// Register on_delete in B 334 | /// Delete B, in B.on_delete: 335 | /// Create C and ensure nothing funky happened to the IDs 336 | #[allow(non_snake_case)] 337 | fn complex_scenario_0() { 338 | let A = TestActivity::new(); 339 | let B = (TestActivity::new(), ()); 340 | let C = (TestActivity::new(), (), ()); 341 | 342 | let id_a = crate::new_activity(A); 343 | 344 | id_a.on_delete(move |_| { 345 | let id_b = crate::new_activity(B); 346 | let num_b: UncheckedActivityId = id_b.into(); 347 | 348 | assert_ne!(num_b, id_a.into()); 349 | 350 | id_b.on_delete(move |_| { 351 | let id_c = crate::new_activity(C); 352 | let num_c: UncheckedActivityId = id_c.into(); 353 | assert_ne!(num_c, num_b); 354 | assert_ne!(num_c, id_a.into()); 355 | }); 356 | id_b.set_status(LifecycleStatus::Deleted); 357 | }); 358 | 359 | id_a.set_status(LifecycleStatus::Deleted); 360 | } 361 | -------------------------------------------------------------------------------- /src/test/lifecycle_tests.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | #[test] 4 | fn active_inactive() { 5 | let a = TestActivity::new(); 6 | let counter = a.shared_counter_ref(); 7 | // Start as not active 8 | let id = crate::new_activity(a); 9 | id.set_status(LifecycleStatus::Inactive); 10 | 11 | // Register for active only 12 | id.subscribe(|activity: &mut TestActivity, _msg: &TestUpdateMsg| { 13 | activity.inc(1); 14 | }); 15 | 16 | crate::publish(TestUpdateMsg); 17 | assert_eq!(counter.get(), 0, "Called inactive activity"); 18 | id.set_status(LifecycleStatus::Active); 19 | crate::publish(TestUpdateMsg); 20 | assert_eq!(counter.get(), 1, "Activation for activity didn't work"); 21 | } 22 | 23 | #[test] 24 | fn enter_leave() { 25 | let a = TestActivity::new(); 26 | let counter = a.shared_counter_ref(); 27 | let id = crate::new_activity(a); 28 | 29 | id.on_enter(|activity: &mut TestActivity| { 30 | activity.inc(1); 31 | }); 32 | id.on_leave(|activity: &mut TestActivity| { 33 | activity.inc(10); 34 | }); 35 | 36 | crate::publish(TestUpdateMsg); 37 | assert_eq!(counter.get(), 0, "Called enter/leave without status change"); 38 | 39 | id.set_status(LifecycleStatus::Inactive); 40 | assert_eq!(counter.get(), 10); 41 | 42 | id.set_status(LifecycleStatus::Active); 43 | assert_eq!(counter.get(), 11); 44 | } 45 | 46 | #[test] 47 | fn set_status_inside_publish() { 48 | let a = TestActivity::new(); 49 | let counter = a.shared_counter_ref(); 50 | let id = crate::new_activity(a); 51 | id.subscribe(move |activity, _msg: &TestUpdateMsg| { 52 | activity.inc(1); 53 | id.set_status(LifecycleStatus::Inactive); 54 | }); 55 | crate::publish(TestUpdateMsg); 56 | crate::publish(TestUpdateMsg); 57 | crate::publish(TestUpdateMsg); 58 | assert_eq!(1, counter.get()); 59 | } 60 | 61 | #[test] 62 | fn on_delete() { 63 | let a = TestActivity::new(); 64 | let counter = a.shared_counter_ref(); 65 | let id = crate::new_activity(a); 66 | id.on_delete(|a| a.inc(1)); 67 | assert_eq!(0, counter.get()); 68 | 69 | id.set_status(LifecycleStatus::Deleted); 70 | assert_eq!(1, counter.get()); 71 | } 72 | 73 | #[test] 74 | fn on_delete_domained() { 75 | let a = TestActivity::new(); 76 | let counter = a.shared_counter_ref(); 77 | let d = TestDomains::DomainA; 78 | crate::store_to_domain(&d, 7usize); 79 | 80 | let id = crate::new_domained_activity(a, &d); 81 | id.on_delete_domained(|a, domain| { 82 | let x: usize = *domain.get(); 83 | assert_eq!(7, x); 84 | a.inc(1) 85 | }); 86 | assert_eq!(0, counter.get()); // Make sure subscription has not been called, yet 87 | 88 | id.set_status(LifecycleStatus::Deleted); 89 | 90 | assert_eq!(1, counter.get()); // Make sure subscription has been called 91 | } 92 | 93 | #[test] 94 | fn delete_without_handler() { 95 | let aid = crate::new_activity(()); 96 | aid.set_status(LifecycleStatus::Deleted); 97 | } 98 | 99 | #[test] 100 | fn delete_twice() { 101 | let aid = crate::new_activity(()); 102 | aid.set_status(LifecycleStatus::Deleted); 103 | aid.set_status(LifecycleStatus::Deleted); 104 | } 105 | 106 | #[test] 107 | #[should_panic] 108 | fn activate_after_delete() { 109 | let a = TestActivity::new(); 110 | let d = TestDomains::DomainA; 111 | let id = crate::new_domained_activity(a, &d); 112 | id.set_status(LifecycleStatus::Deleted); 113 | id.set_status(LifecycleStatus::Active); 114 | } 115 | 116 | #[test] 117 | // Subscription handlers should not be called after an activity has been deleted 118 | fn call_subscription_after_delete() { 119 | let a = TestActivity::new(); 120 | let counter = a.shared_counter_ref(); 121 | let d = TestDomains::DomainA; 122 | 123 | let id = crate::new_domained_activity(a, &d); 124 | id.subscribe_domained(|a, _domain, _msg: &TestMessage| a.inc(1)); 125 | assert_eq!(0, counter.get()); // Make sure subscription has not been called, yet 126 | 127 | // Make sure subscription has ben registered properly 128 | crate::publish(TestMessage(0)); 129 | assert_eq!(1, counter.get()); 130 | // Make sure subscription is no longer called 131 | id.set_status(LifecycleStatus::Deleted); 132 | crate::publish(TestMessage(0)); 133 | assert_eq!(1, counter.get()); 134 | } 135 | 136 | #[test] 137 | // Subscription handlers should not be called after an activity has been deleted, regardless of it being executed deferred or not 138 | fn deferred_delete_with_hanging_subscription() { 139 | let outer = crate::new_activity(()); 140 | let a = TestActivity::new(); 141 | let counter = a.shared_counter_ref(); 142 | let counter_clone = counter.clone(); 143 | let id = crate::new_activity(a); 144 | id.subscribe(|a, msg: &TestMessage| a.inc(msg.0)); 145 | outer.subscribe(move |_, &()| { 146 | assert_eq!(0, counter_clone.get()); 147 | // This should be called but deferred 148 | crate::publish(TestMessage(1)); 149 | assert_eq!(0, counter_clone.get()); 150 | 151 | id.set_status(LifecycleStatus::Deleted); 152 | 153 | // This should not be called at all 154 | crate::publish(TestMessage(10)); 155 | assert_eq!(0, counter_clone.get()); 156 | }); 157 | assert_eq!(0, counter.get()); 158 | crate::publish(()); 159 | assert_eq!(1, counter.get()); 160 | } 161 | 162 | #[test] 163 | // Subscriptions to a deleted activity should be ignored 164 | fn subscribe_after_delete() { 165 | let a = TestActivity::new(); 166 | let counter = a.shared_counter_ref(); 167 | let d = TestDomains::DomainA; 168 | 169 | let id = crate::new_domained_activity(a, &d); 170 | id.set_status(LifecycleStatus::Deleted); 171 | 172 | id.subscribe_domained(|a, _domain, _msg: &TestMessage| a.inc(1)); 173 | assert_eq!(0, counter.get()); // Make sure subscription has not been called, yet 174 | // Make sure subscription is not called 175 | crate::publish(TestMessage(0)); 176 | assert_eq!(0, counter.get()); 177 | } 178 | 179 | #[test] 180 | fn delete_with_on_leave() { 181 | let a = TestActivity::new(); 182 | let counter = a.shared_counter_ref(); 183 | let d = TestDomains::DomainA; 184 | 185 | let id = crate::new_domained_activity(a, &d); 186 | id.on_leave_domained(|a, _domain| a.inc(1)); 187 | 188 | assert_eq!(0, counter.get()); 189 | id.set_status(LifecycleStatus::Deleted); 190 | assert_eq!(1, counter.get()); 191 | } 192 | 193 | #[test] 194 | fn create_new_after_delete() { 195 | let a = TestActivity::new(); 196 | let id_a = crate::new_activity(a); 197 | id_a.set_status(LifecycleStatus::Deleted); 198 | let num_a: UncheckedActivityId = id_a.into(); 199 | 200 | let b = (TestActivity::new(),); 201 | let id_b = crate::new_activity(b); 202 | let num_b: UncheckedActivityId = id_b.into(); 203 | 204 | assert_ne!(num_a, num_b); 205 | } 206 | --------------------------------------------------------------------------------