Index: Cargo.toml ================================================================== --- Cargo.toml +++ Cargo.toml @@ -1,10 +1,11 @@ [package] name = "ump" -version = "0.12.1" +version = "0.13.0" edition = "2021" license = "0BSD" +# https://crates.io/category_slugs categories = [ "concurrency", "asynchronous" ] keywords = [ "channel", "threads", "sync", "message-passing" ] repository = "https://repos.qrnch.tech/pub/ump" description = "Micro message passing library for threads/tasks communication." rust-version = "1.56" @@ -12,27 +13,28 @@ ".fossil-settings", ".efiles", ".fslckout", "examples", "www", + "bacon.toml", "rustfmt.toml" ] -[features] -dev-docs = [] +# https://doc.rust-lang.org/cargo/reference/manifest.html#the-badges-section +[badges] +maintenance = { status = "passively-maintained" } [dependencies] -parking_lot = { version = "0.12.1" } -sigq = { version = "0.13.4" } -swctx = { version = "0.2.1" } +sigq = { version = "0.13.5" } +swctx = { version = "0.3.0" } [dev-dependencies] criterion = { version = "0.5.1", features = ["async_tokio"] } -tokio = { version = "1.32.0", features = ["rt-multi-thread"] } +tokio = { version = "1.40.0", features = ["rt-multi-thread"] } [[bench]] name = "add_server" harness = false [package.metadata.docs.rs] rustdoc-args = ["--generate-link-to-definition"] Index: README.md ================================================================== --- README.md +++ README.md @@ -2,5 +2,13 @@ The _ump_ crate is a simple client/server message passing library for intra-process communication. Its primary purpose is to allow cross async/non-async communication (for both the server and client endpoints). +[![Crates.io][crates-badge]][crates-url] +[![0BSD licensed][0bsd-badge]][0bsd-url] + +[crates-badge]: https://img.shields.io/crates/v/ump.svg +[crates-url]: https://crates.io/crates/ump +[0bsd-badge]: https://img.shields.io/badge/license-0BSD-blue.svg +[0bsd-url]: https://opensource.org/license/0bsd + ADDED bacon.toml Index: bacon.toml ================================================================== --- /dev/null +++ bacon.toml @@ -0,0 +1,130 @@ +# This is a configuration file for the bacon tool +# +# Bacon repository: https://github.com/Canop/bacon +# Complete help on configuration: https://dystroy.org/bacon/config/ +# You can also check bacon's own bacon.toml file +# as an example: https://github.com/Canop/bacon/blob/main/bacon.toml + +# For information about clippy lints, see: +# https://github.com/rust-lang/rust-clippy/blob/master/README.md + +#default_job = "check" +default_job = "clippy-all-pedantic" + +[jobs.check] +command = ["cargo", "check", "--color", "always"] +need_stdout = false + +[jobs.check-all] +command = ["cargo", "check", "--all-targets", "--color", "always"] +need_stdout = false + +# Run clippy on the default target +[jobs.clippy] +command = [ + "cargo", "clippy", + "--color", "always", +] +need_stdout = false + +# Run clippy on all targets +# To disable some lints, you may change the job this way: +# [jobs.clippy-all] +# command = [ +# "cargo", "clippy", +# "--all-targets", +# "--color", "always", +# "--", +# "-A", "clippy::bool_to_int_with_if", +# "-A", "clippy::collapsible_if", +# "-A", "clippy::derive_partial_eq_without_eq", +# ] +# need_stdout = false +[jobs.clippy-all] +command = [ + "cargo", "clippy", + "--all-targets", + "--color", "always", +] +need_stdout = false + +[jobs.clippy-pedantic] +command = [ + "cargo", "clippy", + "--color", "always", + "--", + "-Wclippy::all", + "-Wclippy::pedantic", + "-Wclippy::nursery", + "-Wclippy::cargo" +] +need_stdout = false + +[jobs.clippy-all-pedantic] +command = [ + "cargo", "clippy", + "--all-targets", + "--color", "always", + "--", + "-Wclippy::all", + "-Wclippy::pedantic", + "-Wclippy::nursery", + "-Wclippy::cargo" +] +need_stdout = false + +# This job lets you run +# - all tests: bacon test +# - a specific test: bacon test -- config::test_default_files +# - the tests of a package: bacon test -- -- -p config +[jobs.test] +command = [ + "cargo", "test", "--color", "always", + "--", "--color", "always", # see https://github.com/Canop/bacon/issues/124 +] +need_stdout = true + +[jobs.doc] +command = ["cargo", "doc", "--color", "always", "--no-deps"] +need_stdout = false + +# If the doc compiles, then it opens in your browser and bacon switches +# to the previous job +[jobs.doc-open] +command = ["cargo", "doc", "--color", "always", "--no-deps", "--open"] +need_stdout = false +on_success = "back" # so that we don't open the browser at each change + +# You can run your application and have the result displayed in bacon, +# *if* it makes sense for this crate. +# Don't forget the `--color always` part or the errors won't be +# properly parsed. +# If your program never stops (eg a server), you may set `background` +# to false to have the cargo run output immediately displayed instead +# of waiting for program's end. +[jobs.run] +command = [ + "cargo", "run", + "--color", "always", + # put launch parameters for your program behind a `--` separator +] +need_stdout = true +allow_warnings = true +background = true + +# This parameterized job runs the example of your choice, as soon +# as the code compiles. +# Call it as +# bacon ex -- my-example +[jobs.ex] +command = ["cargo", "run", "--color", "always", "--example"] +need_stdout = true +allow_warnings = true + +# You may define here keybindings that would be specific to +# a project, for example a shortcut to launch a specific job. +# Shortcuts to internal functions (scrolling, toggling, etc.) +# should go in your personal global prefs.toml file instead. +[keybindings] +# alt-m = "job:my-job" +c = "job:clippy-all" # comment this to have 'c' run clippy on only the default target Index: benches/add_server.rs ================================================================== --- benches/add_server.rs +++ benches/add_server.rs @@ -8,10 +8,11 @@ Die, Add(i32, i32), AddThreaded(i32, i32) } +#[allow(clippy::missing_panics_doc)] pub fn criterion_benchmark(c: &mut Criterion) { let mut group = c.benchmark_group("req operation"); let (server, client) = channel::(); @@ -41,11 +42,11 @@ b.iter(|| { p += 2; q -= 3; let result = client.req(Ops::Add(p, q)).unwrap(); assert_eq!(result, q + p); - }) + }); }); p = 0; q = 0; group.bench_function("add (threaded)", |b| { @@ -52,11 +53,11 @@ b.iter(|| { p += 2; q -= 3; let result = client.req(Ops::AddThreaded(p, q)).unwrap(); assert_eq!(result, q + p); - }) + }); }); let rt = tokio::runtime::Runtime::new().unwrap(); group.bench_function("add (async)", |b| { @@ -64,11 +65,11 @@ let p = 1; let q = 2; let result = client.areq(Ops::Add(p, q)).await.unwrap(); assert_eq!(result, q + p); - }) + }); }); let rt = tokio::runtime::Runtime::new().unwrap(); group.bench_function("add (async, threaded)", |b| { @@ -76,11 +77,11 @@ let p = 1; let q = 2; let result = client.areq(Ops::AddThreaded(p, q)).await.unwrap(); assert_eq!(result, q + p); - }) + }); }); let result = client.req(Ops::Die).unwrap(); assert_eq!(result, 0); Index: src/client.rs ================================================================== --- src/client.rs +++ src/client.rs @@ -1,6 +1,6 @@ -use crate::{err::Error, server::ServerQueueNode}; +use crate::{err::Error, server::QueueNode as ServerQueueNode}; use super::rctx::RCtxState; /// Representation of a clonable client object that can issue requests to /// [`Server`](super::Server) objects. @@ -22,29 +22,30 @@ /// /// A complete round-trip (the message is delivered to the server, and the /// server sends a reply) must complete before this function returns /// success. /// + /// # Return + /// On success the function will return `Ok(msg)`. + /// + /// # Errors + /// If the linked server object has been released, or is released while the + /// message is in the server's queue, [`Error::ServerDisappeared`] will be + /// returned. + /// + /// If the server never replied to the message and the reply context was + /// dropped [`Error::NoReply`] will be returned. + /// + /// If an application specific error occurs it will be returned as a + /// [`Error::App`]. + /// + /// # Implementation details /// This method is _currently_ reentrant: It is safe to use share a single /// `Client` among multiple threads. _This may change in the future_; it's /// recommended to not rely on this. The recommended way to send messages to /// a server from multiple threads is to clone the `Client` and assign one /// separate `Client` to each thread. - /// - /// # Return - /// On success the function will return `Ok(msg)`. - /// - /// If the linked server object has been released, or is released while the - /// message is in the server's queue, `Err(Error:ServerDisappeared)` will be - /// returned. - /// - /// If the server never replied to the message and the reply context was - /// dropped `Err(Error::NoReply)` will be returned. - /// - /// If an application specific error occurs it will be returned as a - /// `Err(Error::App(E))`, where `E` is the error type used when creating the - /// [`channel`](crate::channel). pub fn req(&self, out: S) -> Result> { // Create a per-call reply context. // This context could be created when the Client object is being created // and stored in the context, and thus be reused for reach client call. // One side-effect is that some of the state semantics becomes more @@ -112,11 +113,19 @@ /// println!("Client received reply '{}'", reply); /// println!("Client done"); /// /// server_thread.join().unwrap(); /// ``` - pub fn req_async(&self, out: S) -> Result, Error> { + /// + /// # Errors + /// If the linked server object has been released, or is released while the + /// message is in the server's queue, [`Error::ServerDisappeared`] will be + /// returned. + pub fn req_async(&self, out: S) -> Result, Error> + where + S: Send + { let (sctx, wctx) = swctx::mkpair(); self .0 .push(ServerQueueNode { msg: out, @@ -124,17 +133,22 @@ }) .map_err(|_| Error::ServerDisappeared)?; Ok(WaitReply(wctx)) } + #[allow(clippy::missing_errors_doc)] #[deprecated(since = "0.10.2", note = "Use req() instead.")] pub fn send(&self, out: S) -> Result> { self.req(out) } /// Same as [`Client::req()`] but for use in `async` contexts. - pub async fn areq(&self, out: S) -> Result> { + #[allow(clippy::missing_errors_doc)] + pub async fn areq(&self, out: S) -> Result> + where + S: Send + { let (sctx, wctx) = swctx::mkpair(); self .0 .push(ServerQueueNode { @@ -145,17 +159,22 @@ Ok(wctx.wait_async().await?) } #[deprecated(since = "0.10.2", note = "Use areq() instead.")] - pub async fn asend(&self, out: S) -> Result> { + #[allow(clippy::missing_errors_doc)] + pub async fn asend(&self, out: S) -> Result> + where + S: Send + { self.areq(out).await } /// Create a weak `Client` reference. - pub fn weak(&self) -> WeakClient { - WeakClient(self.0.weak()) + #[must_use] + pub fn weak(&self) -> Weak { + Weak(self.0.weak()) } } impl Clone for Client { @@ -166,11 +185,11 @@ /// /// This means that a cloned client can be passed to a new thread/task and /// make new independent calls to the server without any risk of collision /// between clone and the original client object. fn clone(&self) -> Self { - Client(self.0.clone()) + Self(self.0.clone()) } } /// Context used to wait for a server to reply to a request. pub struct WaitReply(swctx::WaitCtx); @@ -177,37 +196,52 @@ impl WaitReply { /// Block and wait for a reply. /// /// For use in non-`async` threads. + /// + /// # Errors + /// [`Error::ServerDisappeared`] means the linked server object has been + /// released. + /// + /// If the [`ReplyContext`](super::ReplyContext) is dropped by the server + /// handler it replies to the message, [`Error::NoReply`] will be returned. + /// + /// If an application specific error occurs it will be returned in + /// [`Error::App`]. pub fn wait(self) -> Result> { Ok(self.0.wait()?) } /// Block and wait for a reply. /// - /// For use in `async` tasks. - pub async fn wait_async(self) -> Result> { + /// Same as [`WaitReply::wait()`], but for use in `async` contexts. + #[allow(clippy::missing_errors_doc)] + pub async fn wait_async(self) -> Result> + where + R: Send, + E: Send + { Ok(self.0.wait_async().await?) } } /// A weak client reference that can be upgraded to a [`Client`] as long as /// other `Client` objects till exist. #[repr(transparent)] -pub struct WeakClient( +pub struct Weak( pub(crate) sigq::WeakPusher> ); -impl Clone for WeakClient { +impl Clone for Weak { fn clone(&self) -> Self { Self(self.0.clone()) } } -impl WeakClient { +impl Weak { /// Upgrade a `WeakClient` to a [`Client`]. /// /// If no strong `Client` objects still exist then `None` is returned. /// /// # Examples @@ -232,11 +266,12 @@ /// drop(client); /// let Some(_) = weak_client.upgrade() else { /// panic!("Unexpectedly able to upgrade weak client"); /// }; /// ``` + #[must_use] pub fn upgrade(&self) -> Option> { self.0.upgrade().map(|x| Client(x)) } } // vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 : Index: src/err.rs ================================================================== --- src/err.rs +++ src/err.rs @@ -10,10 +10,14 @@ /// Happens when clients: /// - attempt to transmit messages to a server that has been deallocated. /// - have their requests dropped from the serrver's queue because the /// server itself was deallocated. ServerDisappeared, + + /// A message reply could not be completed because the original requestor + /// disappearing. + OriginDisappeared, /// No more client end-points remain. /// /// There are no more nodes to pick up in the queue and all client /// end-points have been dropped (implied: no new nodes will ever be added @@ -33,48 +37,53 @@ impl Error { /// Attempt to convert [`Error`] into application-specific error. pub fn into_apperr(self) -> Option { match self { - Error::App(e) => Some(e), + Self::App(e) => Some(e), _ => None } } /// Unwrap application-specific error from an [`Error`]. /// - /// # Panic + /// # Panics /// Panics if `Error` variant is not `Error::App()`. pub fn unwrap_apperr(self) -> E { match self { - Error::App(e) => e, + Self::App(e) => e, _ => panic!("Not an Error::App") } } } -impl std::error::Error for Error {} +impl std::error::Error for Error {} -impl fmt::Display for Error { +impl fmt::Display for Error +where + E: std::error::Error +{ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - Error::ServerDisappeared => write!(f, "Server disappeared"), - Error::ClientsDisappeared => write!(f, "Clients disappeared"), - Error::NoReply => write!(f, "Server didn't reply"), - Error::App(err) => write!(f, "Application error; {:?}", err) + Self::ServerDisappeared => write!(f, "Server disappeared"), + Self::OriginDisappeared => write!(f, "Requestor disappeared"), + Self::ClientsDisappeared => write!(f, "Clients disappeared"), + Self::NoReply => write!(f, "Server didn't reply"), + Self::App(err) => write!(f, "Application error; {}", err) } } } impl From> for Error { fn from(err: swctx::Error) -> Self { match err { swctx::Error::Aborted(state) => match state { - RCtxState::Queued => Error::ServerDisappeared, - RCtxState::Active => Error::NoReply + RCtxState::Queued => Self::ServerDisappeared, + RCtxState::Active => Self::NoReply }, - swctx::Error::App(e) => Error::App(e) + swctx::Error::LostWaiter => Self::OriginDisappeared, + swctx::Error::App(e) => Self::App(e) } } } // vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 : Index: src/lib.rs ================================================================== --- src/lib.rs +++ src/lib.rs @@ -105,11 +105,11 @@ mod server; pub use err::Error; pub use crate::{ - client::{Client, WaitReply, WeakClient}, + client::{Client, WaitReply, Weak as WeakClient}, rctx::ReplyContext, server::Server }; /// Create a pair of linked [`Server`] and [`Client`] objects. @@ -128,10 +128,11 @@ /// /// The `S` type parameter is the "request" data type that clients will /// transfer to the server. The `R` type parameter is the "receive" data type /// that clients will receive from the server. The `E` type parameter can be /// used to return application specific errors from the server to the client. +#[must_use] pub fn channel() -> (Server, Client) { let (qpusher, qpuller) = sigq::new(); let server = Server(qpuller); let client = Client(qpusher); Index: src/rctx.rs ================================================================== --- src/rctx.rs +++ src/rctx.rs @@ -6,11 +6,11 @@ //! another, where the receiver will block until data has been delivered. use crate::err::Error; #[derive(Clone, Default)] -pub(crate) enum RCtxState { +pub enum RCtxState { #[default] Queued, Active } @@ -36,14 +36,18 @@ /// let reply = client.req(msg).unwrap(); /// assert_eq!(reply, "Hello, Client!"); /// server_thread.join().unwrap(); /// ``` /// + /// # Errors + /// If the originating caller is no longer waiting for a reply (i.e. was + /// dropped) [`Error::OriginDisappeared`] is returned. + /// /// # Semantics /// This call is safe to make after the server context has been released. pub fn reply(self, data: T) -> Result<(), Error> { - self.0.set(data); + self.0.set(data)?; Ok(()) } /// Return an error to originating client. /// This will cause the calling client to return an error. The error passed @@ -76,21 +80,32 @@ /// } /// } /// server_thread.join().unwrap(); /// ``` /// + /// # Errors + /// If the originating caller is no longer waiting for a reply (i.e. was + /// dropped) [`Error::OriginDisappeared`] is returned. + /// /// # Semantics /// This call is safe to make after the server context has been released. pub fn fail(self, err: E) -> Result<(), Error> { - self.0.fail(err); + self.0.fail(err)?; Ok(()) } } -impl From> for ReplyContext { - fn from(sctx: swctx::SetCtx) -> Self { - sctx.set_state(RCtxState::Active); - ReplyContext(sctx) +impl TryFrom> for ReplyContext { + type Error = Error; + + /// Convert a `SetCtx` into a `ReplyContext`. + /// + /// Sets the `SetCtx`'s stat to _Active_. + fn try_from( + sctx: swctx::SetCtx + ) -> Result { + let _ = sctx.set_state(RCtxState::Active); + Ok(Self(sctx)) } } // vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 : Index: src/server.rs ================================================================== --- src/server.rs +++ src/server.rs @@ -1,11 +1,11 @@ use crate::{ err::Error, rctx::{RCtxState, ReplyContext} }; -pub(crate) struct ServerQueueNode { +pub struct QueueNode { /// Raw message being sent from the client to the server. pub(crate) msg: S, /// Keep track of data needed to share reply data. pub(crate) reply: swctx::SetCtx @@ -15,11 +15,11 @@ /// /// Each instantiation of a [`Server`] object represents an end-point which /// will be used to receive messages from connected [`Client`](crate::Client) /// objects. #[repr(transparent)] -pub struct Server(pub(crate) sigq::Puller>); +pub struct Server(pub(crate) sigq::Puller>); impl Server where S: 'static + Send, R: 'static + Send, @@ -29,24 +29,32 @@ /// [`Client`](crate::Client). /// /// Returns the message sent by the client and a reply context. The server /// must call [`ReplyContext::reply()`] on the reply context to pass a return /// value to the client. + /// + /// # Errors + /// `Err(Error::ClientsDisappeared)` indicates that the queue is empty and + /// all the client end-points have been dropped. pub fn wait(&self) -> Result<(S, ReplyContext), Error> { let node = self.0.pop().map_err(|_| Error::ClientsDisappeared)?; // Extract the data from the node let msg = node.msg; // Create an application reply context from the reply context in the queue // Implicitly changes state of the reply context from Queued to Waiting - let rctx = ReplyContext::from(node.reply); + let rctx = ReplyContext::try_from(node.reply)?; Ok((msg, rctx)) } /// Take next next message off queue or return `None` is queue is empty. + /// + /// # Errors + /// [`Error::ClientsDisappeared`] indicates that the queue is empty and + /// all the client end-points have been dropped. #[allow(clippy::type_complexity)] pub fn try_pop(&self) -> Result)>, Error> { let node = self.0.try_pop().map_err(|_| Error::ClientsDisappeared)?; if let Some(node) = node { @@ -54,36 +62,38 @@ let msg = node.msg; // Create an application reply context from the reply context in the // queue Implicitly changes state of the reply context from Queued // to Waiting - let rctx = ReplyContext::from(node.reply); + let rctx = ReplyContext::try_from(node.reply)?; Ok(Some((msg, rctx))) } else { Ok(None) } } /// Same as [`Server::wait()`], but for use in an `async` context. + #[allow(clippy::missing_errors_doc)] pub async fn async_wait(&self) -> Result<(S, ReplyContext), Error> { let node = self.0.apop().await.map_err(|_| Error::ClientsDisappeared)?; // Extract the data from the node let msg = node.msg; // Create an application reply context from the reply context in the queue // Implicitly changes state of the reply context from Queued to Waiting - let rctx = ReplyContext::from(node.reply); + let rctx = ReplyContext::try_from(node.reply)?; Ok((msg, rctx)) } /// Returns a boolean indicating whether the queue is/was empty. This isn't /// really useful unless used in very specific situations. It mostly exists /// for test cases. + #[must_use] pub fn was_empty(&self) -> bool { self.0.was_empty() } } // vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 : Index: tests/async_client.rs ================================================================== --- tests/async_client.rs +++ tests/async_client.rs @@ -44,14 +44,13 @@ } else { panic!("Didn't get sum"); } } let result = client.areq(Request::Croak).await.unwrap(); - if let Reply::OkICroaked = result { - } else { + let Reply::OkICroaked = result else { panic!("Didn't get a croak"); - } + }; }); server_thread.join().unwrap(); } ADDED tests/wait_disappear.rs Index: tests/wait_disappear.rs ================================================================== --- /dev/null +++ tests/wait_disappear.rs @@ -0,0 +1,41 @@ +use ump::{channel, Error}; + +#[test] +fn wait_disappered_on_reply() { + let (server, client) = channel::(); + + // Generate a request that will return a wait context. + let wctx = client.req_async(String::from("hello")); + + // Get the message (and reply context) from server end-point + let (_msg, rctx) = server.wait().unwrap(); + + // nuke the wait context + drop(wctx); + + // Replying should fail, because wctx has been dropped + let Err(Error::OriginDisappeared) = rctx.reply(String::from("ahoy")) else { + panic!("Unexpected error"); + }; +} + +#[test] +fn wait_disappered_on_fail() { + let (server, client) = channel::(); + + // Generate a request that will return a wait context. + let wctx = client.req_async(String::from("hello")); + + // Get the message (and reply context) from server end-point + let (_msg, rctx) = server.wait().unwrap(); + + // nuke the wait context + drop(wctx); + + // Failing should fail, because wctx has been dropped + let Err(Error::OriginDisappeared) = rctx.fail(()) else { + panic!("Unexpected error"); + }; +} + +// vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 : Index: www/changelog.md ================================================================== --- www/changelog.md +++ www/changelog.md @@ -1,18 +1,43 @@ # Change Log +⚠️ indicates a breaking change. + ## [Unreleased] + +[Details](/vdiff?from=ump-0.13.0&to=trunk) ### Added ### Changed ### Removed --- + +## [0.13.0] - 2024-09-10 + +[Details](/vdiff?from=ump-0.12.1&to=ump-0.13.0) + +### Changed + +- Update to `swctx` to `0.3.0`, allowing `ReplyContext` to detect if the + originating client has been dropped. +- ⚠️ Require `std::error::Error` bound on application-specific error `E` for + `std::error::Error` implementation on `Error` as well as `fmt::Display` + for `Error`. + +### Removed + +- Remove `dev-docs` feature +- Remove superfluous `parking_lot` dependency. + +--- ## [0.12.1] - 2023-10-02 + +[Details](/vdiff?from=ump-0.12.0&to=ump-0.12.1) ### Added - Add `Client::req_async()`. - Add `Server::try_pop()`. @@ -22,11 +47,11 @@ --- ## [0.12.0] - 2023-08-15 -[Details](https://repos.qrnch.tech/pub/ump/vdiff?from=ump-0.11.0&to=ump-0.12.0) +[Details](/vdiff?from=ump-0.11.0&to=ump-0.12.0) ### Changed - Include tests when publishing crate. - Bugfix: Use `err::Error` rather than `rctx::err::Error` in rctx::public, @@ -39,11 +64,11 @@ --- ## [0.11.0] - 2023-07-29 -[Details](https://repos.qrnch.tech/pub/ump/vdiff?from=ump-0.10.2&to=ump-0.11.0) +[Details](/vdiff?from=ump-0.10.2&to=ump-0.11.0) ### Changed - Include tests when publishing crate. - Bugfix: Use `err::Error` rather than `rctx::err::Error` in rctx::public, @@ -52,11 +77,11 @@ --- ## [0.10.2] - 2023-07-28 -[Details](https://repos.qrnch.tech/pub/ump/vdiff?from=ump-0.10.1&to=ump-0.10.2) +[Details](/vdiff?from=ump-0.10.1&to=ump-0.10.2) ### Added - Add `send()`/`asend()` wrappers around the new `req()`/`areq()` methods with a deprecation notice. @@ -69,11 +94,11 @@ --- ## [0.10.1] - 2023-07-27 -[Details](https://repos.qrnch.tech/pub/ump/vdiff?from=ump-0.10.0&to=ump-0.10.1) +[Details](/vdiff?from=ump-0.10.0&to=ump-0.10.1) ### Changed - Runtime dependencies: - Updated `sigq` to `0.13.3`. @@ -80,11 +105,11 @@ --- ## [0.10.0] - 2023-07-26 -[Details](https://repos.qrnch.tech/pub/ump/vdiff?from=ump-0.9.0&to=ump-0.10.0) +[Details](/vdiff?from=ump-0.9.0&to=ump-0.10.0) ### Added - Server's receive methods will fail with `Error::ClientsDisappeared` if all the associated Client objects have been dropped.