From fa9f1fbfb448bf0613165ce13dca816d1c5de708 Mon Sep 17 00:00:00 2001 From: pgherveou Date: Fri, 24 Jul 2026 17:44:30 +0200 Subject: [PATCH] refactor(api): adopt Send async traits Native executors need dispatch futures that can move across Tokio worker threads. Keep concise async signatures while teaching rustdoc codegen to unwrap async-trait futures. --- Cargo.lock | 1 + rust/crates/truapi-codegen/src/rustdoc.rs | 207 +++++++++++++++++- rust/crates/truapi-server/src/dispatcher.rs | 23 +- rust/crates/truapi-server/src/host_core.rs | 19 ++ rust/crates/truapi-server/src/runtime.rs | 14 ++ .../src/runtime/statement_store.rs | 1 + rust/crates/truapi-server/src/subscription.rs | 4 +- rust/crates/truapi/Cargo.toml | 1 + rust/crates/truapi/src/api/account.rs | 1 + rust/crates/truapi/src/api/chain.rs | 1 + rust/crates/truapi/src/api/chat.rs | 1 + rust/crates/truapi/src/api/coin_payment.rs | 1 + rust/crates/truapi/src/api/entropy.rs | 1 + rust/crates/truapi/src/api/local_storage.rs | 1 + rust/crates/truapi/src/api/notifications.rs | 1 + rust/crates/truapi/src/api/payment.rs | 1 + rust/crates/truapi/src/api/permissions.rs | 1 + rust/crates/truapi/src/api/preimage.rs | 1 + .../truapi/src/api/resource_allocation.rs | 1 + rust/crates/truapi/src/api/signing.rs | 1 + rust/crates/truapi/src/api/statement_store.rs | 1 + rust/crates/truapi/src/api/system.rs | 1 + rust/crates/truapi/src/api/theme.rs | 1 + rust/crates/truapi/src/lib.rs | 7 +- 24 files changed, 276 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 30c55676..4ba5ec08 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3961,6 +3961,7 @@ dependencies = [ name = "truapi" version = "0.5.1" dependencies = [ + "async-trait", "derive_more 2.1.1", "futures", "hex", diff --git a/rust/crates/truapi-codegen/src/rustdoc.rs b/rust/crates/truapi-codegen/src/rustdoc.rs index 1a51a671..933e23ef 100644 --- a/rust/crates/truapi-codegen/src/rustdoc.rs +++ b/rust/crates/truapi-codegen/src/rustdoc.rs @@ -650,7 +650,8 @@ fn extract_method(item_id: &str, item: &Item, names: &NameContext) -> Result bool { .unwrap_or(false) } +/// Resolve the `Output = T` binding from a Send future method return. +/// +/// `async_trait` represents `async fn` as +/// `Pin + Send + 'async_trait>>` in rustdoc JSON. +/// Explicit `impl Future + Send` returns are also accepted so the +/// parser remains compatible with older TrUAPI trait snapshots. +fn unwrap_future_output(output: &serde_json::Value) -> Result<&serde_json::Value> { + if let Some(future_output) = extract_async_trait_future_output(output) { + return Ok(future_output); + } + let Some(bounds) = output + .get("impl_trait") + .and_then(serde_json::Value::as_array) + else { + return Ok(output); + }; + let future = bounds + .iter() + .filter_map(|bound| bound.get("trait_bound")) + .filter_map(|bound| bound.get("trait")) + .find(|bound| { + bound + .get("path") + .and_then(serde_json::Value::as_str) + .is_some_and(|path| path_suffix(path) == "Future") + }) + .context("impl Trait return is missing its Future bound")?; + let constraints = future + .get("args") + .and_then(|args| args.get("angle_bracketed")) + .and_then(|args| args.get("constraints")) + .and_then(serde_json::Value::as_array) + .context("Future bound is missing its associated-type constraints")?; + constraints + .iter() + .find(|constraint| { + constraint.get("name").and_then(serde_json::Value::as_str) == Some("Output") + }) + .and_then(|constraint| constraint.get("binding")) + .and_then(|binding| binding.get("equality")) + .and_then(|equality| equality.get("type")) + .context("Future bound is missing its Output equality") +} + +fn extract_async_trait_future_output(output: &serde_json::Value) -> Option<&serde_json::Value> { + let pin = output.get("resolved_path")?; + if resolved_path_leaf(pin) != Some("Pin") { + return None; + } + let boxed = generic_type_arg(pin, 0)?.get("resolved_path")?; + if resolved_path_leaf(boxed) != Some("Box") { + return None; + } + let dyn_trait = generic_type_arg(boxed, 0)?.get("dyn_trait")?; + let traits = dyn_trait.get("traits")?.as_array()?; + traits + .iter() + .filter_map(|entry| entry.get("trait")) + .find(|trait_| resolved_path_leaf(trait_) == Some("Future"))? + .get("args")? + .get("angle_bracketed")? + .get("constraints")? + .as_array()? + .iter() + .find(|constraint| constraint.get("name").and_then(|name| name.as_str()) == Some("Output"))? + .get("binding")? + .get("equality")? + .get("type") +} + +fn resolved_path_leaf(resolved: &serde_json::Value) -> Option<&str> { + let path = resolved.get("path")?.as_str()?; + Some(path_suffix(path)) +} + +fn generic_type_arg(resolved: &serde_json::Value, index: usize) -> Option<&serde_json::Value> { + resolved + .get("args")? + .get("angle_bracketed")? + .get("args")? + .as_array()? + .iter() + .filter_map(|entry| entry.get("type")) + .nth(index) +} + fn is_result_subscription_return(output: &serde_json::Value) -> bool { if !is_result_return(output) { return false; @@ -1398,4 +1485,122 @@ mod tests { "unexpected error: {err}" ); } + + #[test] + fn unwraps_send_future_output() { + let output = serde_json::json!({ + "impl_trait": [ + { + "trait_bound": { + "trait": { + "path": "core::future::Future", + "args": { + "angle_bracketed": { + "args": [], + "constraints": [ + { + "name": "Output", + "binding": { + "equality": { + "type": { + "resolved_path": { + "path": "Result", + "id": 1, + "args": null + } + } + } + } + } + ] + } + } + } + } + }, + { + "trait_bound": { + "trait": { + "path": "Send", + "id": 2, + "args": null + } + } + } + ] + }); + + let unwrapped = unwrap_future_output(&output).expect("future output"); + + assert_eq!(get_resolved_name(unwrapped).as_deref(), Some("Result")); + } + + #[test] + fn unwraps_async_trait_send_future_output() { + let output = serde_json::json!({ + "resolved_path": { + "path": "::core::pin::Pin", + "args": { + "angle_bracketed": { + "args": [{ + "type": { + "resolved_path": { + "path": "Box", + "args": { + "angle_bracketed": { + "args": [{ + "type": { + "dyn_trait": { + "traits": [ + { + "trait": { + "path": "::core::future::Future", + "args": { + "angle_bracketed": { + "args": [], + "constraints": [{ + "name": "Output", + "binding": { + "equality": { + "type": { + "resolved_path": { + "path": "Result", + "id": 1, + "args": null + } + } + } + } + }] + } + } + } + }, + { + "trait": { + "path": "::core::marker::Send", + "args": null + } + } + ], + "lifetime": "'async_trait" + } + } + }], + "constraints": [] + } + } + } + } + }], + "constraints": [] + } + } + } + }); + + let unwrapped = unwrap_future_output(&output).expect("async-trait future output"); + + assert_eq!(get_resolved_name(unwrapped).as_deref(), Some("Result")); + } } diff --git a/rust/crates/truapi-server/src/dispatcher.rs b/rust/crates/truapi-server/src/dispatcher.rs index e0c3db60..45164c61 100644 --- a/rust/crates/truapi-server/src/dispatcher.rs +++ b/rust/crates/truapi-server/src/dispatcher.rs @@ -9,7 +9,7 @@ use std::collections::{HashMap, HashSet}; use std::sync::Arc; -use futures::future::LocalBoxFuture; +use futures::future::BoxFuture; use tracing::instrument; use crate::frame::{Payload, ProtocolMessage}; @@ -17,19 +17,20 @@ use crate::generated::wire_table::{RequestFrameIds, SubscriptionFrameIds}; use crate::subscription::{Spawner, SubscriptionManager, SubscriptionStream}; use crate::transport::Transport; -/// A handler for a request-response method. The returned future is not -/// required to be `Send` because the truapi trait uses `async fn`, whose -/// auto-Send-ness is not guaranteed. The `request_id` is the per-frame -/// identifier; handlers thread it into the `CallContext` so trait methods -/// can correlate logs/cancellation with the originating request. On the -/// error path handlers return the complete SCALE-encoded response payload. +/// A handler for a request-response method. TrUAPI service traits require +/// their returned futures to be [`Send`], allowing native dispatch to move +/// across executor threads while WASM remains free to poll the same future on +/// its local executor. The `request_id` is the per-frame identifier; handlers +/// thread it into the `CallContext` so trait methods can correlate +/// logs/cancellation with the originating request. On the error path handlers +/// return the complete SCALE-encoded response payload. pub type RequestHandler = - Arc) -> LocalBoxFuture<'static, Result, Vec>> + Send + Sync>; + Arc) -> BoxFuture<'static, Result, Vec>> + Send + Sync>; /// A handler for a subscription method. On the error path the handler returns /// the complete SCALE-encoded `_interrupt` payload. pub type SubscriptionHandler = Arc< - dyn Fn(String, Vec) -> LocalBoxFuture<'static, Result>> + dyn Fn(String, Vec) -> BoxFuture<'static, Result>> + Send + Sync, >; @@ -72,7 +73,7 @@ impl Dispatcher { /// since each request id must own exactly one handler. pub fn on_request(&mut self, ids: RequestFrameIds, handler: F) -> Option where - F: Fn(String, Vec) -> LocalBoxFuture<'static, Result, Vec>> + F: Fn(String, Vec) -> BoxFuture<'static, Result, Vec>> + Send + Sync + 'static, @@ -95,7 +96,7 @@ impl Dispatcher { handler: F, ) -> Option where - F: Fn(String, Vec) -> LocalBoxFuture<'static, Result>> + F: Fn(String, Vec) -> BoxFuture<'static, Result>> + Send + Sync + 'static, diff --git a/rust/crates/truapi-server/src/host_core.rs b/rust/crates/truapi-server/src/host_core.rs index ee561f8a..d1e8ccc5 100644 --- a/rust/crates/truapi-server/src/host_core.rs +++ b/rust/crates/truapi-server/src/host_core.rs @@ -580,6 +580,25 @@ mod tests { } } + fn assert_send(_: T) {} + + fn assert_send_sync() {} + + #[test] + fn product_runtime_and_dispatch_future_are_send() { + assert_send_sync::(); + let (host_config, product) = runtime_config("myapp.dot"); + let runtime = ProductRuntime::from_platform_with_config( + Arc::new(StubPlatform::default()), + host_config, + product, + test_spawner(), + Arc::new(RecordingSink::default()), + ); + + assert_send(runtime.receive_frame(Vec::new())); + } + #[test] fn dispose_cancels_active_subscriptions() { let theme_stream_dropped = Arc::new(AtomicBool::new(false)); diff --git a/rust/crates/truapi-server/src/runtime.rs b/rust/crates/truapi-server/src/runtime.rs index a98814b9..c012751d 100644 --- a/rust/crates/truapi-server/src/runtime.rs +++ b/rust/crates/truapi-server/src/runtime.rs @@ -693,6 +693,7 @@ fn runtime_failure_to_call_error(failure: RuntimeFailure) -> CallError { // System // --------------------------------------------------------------------------- +#[truapi::async_trait] impl System for ProductRuntimeHost { #[instrument(skip_all, fields(runtime.method = "system.feature_supported"))] async fn feature_supported( @@ -742,6 +743,7 @@ impl System for ProductRuntimeHost { // Permissions // --------------------------------------------------------------------------- +#[truapi::async_trait] impl Permissions for ProductRuntimeHost { #[instrument(skip_all, fields(runtime.method = "permissions.request_device_permission"))] async fn request_device_permission( @@ -798,6 +800,7 @@ impl Permissions for ProductRuntimeHost { // LocalStorage // --------------------------------------------------------------------------- +#[truapi::async_trait] impl LocalStorage for ProductRuntimeHost { #[instrument(skip_all, fields(runtime.method = "local_storage.read"))] async fn read( @@ -855,6 +858,7 @@ impl LocalStorage for ProductRuntimeHost { // Account-management flows live in the Rust core itself, backed by the shared // session state and, for alias/proof/login success paths, the SSO service. +#[truapi::async_trait] impl Account for ProductRuntimeHost { #[instrument(skip_all, fields(runtime.method = "account.get_account"))] async fn get_account( @@ -1131,6 +1135,7 @@ fn transaction_call_error( })) } +#[truapi::async_trait] impl Signing for ProductRuntimeHost { #[instrument(skip_all, fields(runtime.method = "signing.sign_payload"))] async fn sign_payload( @@ -1512,6 +1517,7 @@ impl Signing for ProductRuntimeHost { // translated into `RemoteChainHeadFollowItem` items on the subscription // stream. +#[truapi::async_trait] impl Chain for ProductRuntimeHost { #[instrument(skip_all, fields(runtime.method = "chain.follow_head_subscribe"))] async fn follow_head_subscribe( @@ -1743,8 +1749,11 @@ impl Chain for ProductRuntimeHost { const PAYMENTS_NOT_IMPLEMENTED: &str = "Payments are not supported in dot.li"; +#[truapi::async_trait] impl Chat for ProductRuntimeHost {} +#[truapi::async_trait] impl CoinPayment for ProductRuntimeHost {} +#[truapi::async_trait] impl Payment for ProductRuntimeHost { #[instrument(skip_all, fields(runtime.method = "payment.balance_subscribe"))] async fn balance_subscribe( @@ -1803,6 +1812,7 @@ impl Payment for ProductRuntimeHost { } } +#[truapi::async_trait] impl ResourceAllocation for ProductRuntimeHost { #[instrument(skip_all, fields(runtime.method = "resource_allocation.request"))] async fn request( @@ -1864,6 +1874,7 @@ impl ResourceAllocation for ProductRuntimeHost { // Entropy // --------------------------------------------------------------------------- +#[truapi::async_trait] impl Entropy for ProductRuntimeHost { #[instrument(skip_all, fields(runtime.method = "entropy.derive"))] async fn derive( @@ -1900,6 +1911,7 @@ impl Entropy for ProductRuntimeHost { // Preimage // --------------------------------------------------------------------------- +#[truapi::async_trait] impl Preimage for ProductRuntimeHost { #[instrument(skip_all, fields(runtime.method = "preimage.lookup_subscribe"))] async fn lookup_subscribe( @@ -2085,6 +2097,7 @@ fn bulletin_allowance_error_reason(err: AuthorityError) -> String { // Theme // --------------------------------------------------------------------------- +#[truapi::async_trait] impl Theme for ProductRuntimeHost { #[instrument(skip_all, fields(runtime.method = "theme.subscribe"))] async fn subscribe(&self, _cx: &CallContext) -> Subscription { @@ -2109,6 +2122,7 @@ impl Theme for ProductRuntimeHost { // `Notifications` delegates to the platform so hosts can own scheduling and // cancellation while the core preserves the typed TrUAPI wire shape. +#[truapi::async_trait] impl Notifications for ProductRuntimeHost { #[instrument(skip_all, fields(runtime.method = "notifications.send_push_notification"))] async fn send_push_notification( diff --git a/rust/crates/truapi-server/src/runtime/statement_store.rs b/rust/crates/truapi-server/src/runtime/statement_store.rs index 7e2bca0f..270dfe1d 100644 --- a/rust/crates/truapi-server/src/runtime/statement_store.rs +++ b/rust/crates/truapi-server/src/runtime/statement_store.rs @@ -35,6 +35,7 @@ use truapi::versioned::statement_store::{ }; use truapi::{CallContext, CallError, Subscription}; +#[truapi::async_trait] impl StatementStore for ProductRuntimeHost { #[instrument(skip_all, fields(runtime.method = "statement_store.subscribe"))] async fn subscribe( diff --git a/rust/crates/truapi-server/src/subscription.rs b/rust/crates/truapi-server/src/subscription.rs index b6766d16..cd026bd0 100644 --- a/rust/crates/truapi-server/src/subscription.rs +++ b/rust/crates/truapi-server/src/subscription.rs @@ -23,8 +23,8 @@ type StopFn = Box; /// future is `Send` because the inner [`SubscriptionStream`] is a /// `BoxStream<'static, _>` and every captured value the manager threads /// through it is also `Send`. Each platform bridge supplies an -/// implementation that hands the future to the runtime driving its -/// transport (tokio `LocalSet`, `wasm_bindgen_futures::spawn_local`, ...). +/// implementation that hands the future to the runtime driving its transport +/// (`tokio::spawn`, `wasm_bindgen_futures::spawn_local`, ...). pub type Spawner = Arc) + Send + Sync>; /// Convenience spawner for tests and embedders that don't yet wire a diff --git a/rust/crates/truapi/Cargo.toml b/rust/crates/truapi/Cargo.toml index 434bd5f3..6d27a071 100644 --- a/rust/crates/truapi/Cargo.toml +++ b/rust/crates/truapi/Cargo.toml @@ -6,6 +6,7 @@ license.workspace = true description = "TrUAPI trait and type definitions" [dependencies] +async-trait = "0.1" derive_more = { version = "2", features = ["display"] } futures = "0.3" hex = "0.4" diff --git a/rust/crates/truapi/src/api/account.rs b/rust/crates/truapi/src/api/account.rs index a3074048..f839ab1a 100644 --- a/rust/crates/truapi/src/api/account.rs +++ b/rust/crates/truapi/src/api/account.rs @@ -13,6 +13,7 @@ use crate::wire; use crate::{CallContext, CallError, Subscription}; /// Account lookup, aliasing, and proof generation. +#[crate::async_trait] pub trait Account: Send + Sync { /// Subscribe to account connection status changes. /// diff --git a/rust/crates/truapi/src/api/chain.rs b/rust/crates/truapi/src/api/chain.rs index 1f47c85b..1b33b210 100644 --- a/rust/crates/truapi/src/api/chain.rs +++ b/rust/crates/truapi/src/api/chain.rs @@ -22,6 +22,7 @@ use crate::wire; use crate::{CallContext, CallError, Subscription}; /// Chain interaction methods. +#[crate::async_trait] pub trait Chain: Send + Sync { /// Follow the chain head and receive block events. /// diff --git a/rust/crates/truapi/src/api/chat.rs b/rust/crates/truapi/src/api/chat.rs index 823d0f68..c9e9e826 100644 --- a/rust/crates/truapi/src/api/chat.rs +++ b/rust/crates/truapi/src/api/chat.rs @@ -11,6 +11,7 @@ use crate::wire; use crate::{CallContext, CallError, Subscription}; /// Chat room, bot, and message APIs. +#[crate::async_trait] pub trait Chat: Send + Sync { /// Create a chat room. /// diff --git a/rust/crates/truapi/src/api/coin_payment.rs b/rust/crates/truapi/src/api/coin_payment.rs index 25ae3ece..5839b8e3 100644 --- a/rust/crates/truapi/src/api/coin_payment.rs +++ b/rust/crates/truapi/src/api/coin_payment.rs @@ -22,6 +22,7 @@ use crate::{CallContext, CallError, Subscription}; /// RFC 0017 describes `Resolvable` values for long-running operations. /// TrUAPI represents those as subscriptions whose items are the RFC status /// updates. +#[crate::async_trait] pub trait CoinPayment: Send + Sync { /// Create a new firewalled CoinPayment purse. /// diff --git a/rust/crates/truapi/src/api/entropy.rs b/rust/crates/truapi/src/api/entropy.rs index 2e52f212..32f510b9 100644 --- a/rust/crates/truapi/src/api/entropy.rs +++ b/rust/crates/truapi/src/api/entropy.rs @@ -7,6 +7,7 @@ use crate::wire; use crate::{CallContext, CallError}; /// Deterministic entropy derivation. +#[crate::async_trait] pub trait Entropy: Send + Sync { /// Derive deterministic entropy. /// diff --git a/rust/crates/truapi/src/api/local_storage.rs b/rust/crates/truapi/src/api/local_storage.rs index 8cf17123..ec0bc634 100644 --- a/rust/crates/truapi/src/api/local_storage.rs +++ b/rust/crates/truapi/src/api/local_storage.rs @@ -9,6 +9,7 @@ use crate::wire; use crate::{CallContext, CallError}; /// Local key/value storage scoped to the calling product. +#[crate::async_trait] pub trait LocalStorage: Send + Sync { /// Read a value by key. /// diff --git a/rust/crates/truapi/src/api/notifications.rs b/rust/crates/truapi/src/api/notifications.rs index 69287bcd..01bf6769 100644 --- a/rust/crates/truapi/src/api/notifications.rs +++ b/rust/crates/truapi/src/api/notifications.rs @@ -9,6 +9,7 @@ use crate::wire; use crate::{CallContext, CallError}; /// Notification methods for locally-rendered push notifications. +#[crate::async_trait] pub trait Notifications: Send + Sync { /// Send a push notification to the user. /// diff --git a/rust/crates/truapi/src/api/payment.rs b/rust/crates/truapi/src/api/payment.rs index cd1d3aae..55f53b19 100644 --- a/rust/crates/truapi/src/api/payment.rs +++ b/rust/crates/truapi/src/api/payment.rs @@ -11,6 +11,7 @@ use crate::wire; use crate::{CallContext, CallError, Subscription}; /// Payment request and balance/status subscription methods. +#[crate::async_trait] pub trait Payment: Send + Sync { /// Subscribe to payment balance updates. /// diff --git a/rust/crates/truapi/src/api/permissions.rs b/rust/crates/truapi/src/api/permissions.rs index 60fc685f..a190984d 100644 --- a/rust/crates/truapi/src/api/permissions.rs +++ b/rust/crates/truapi/src/api/permissions.rs @@ -8,6 +8,7 @@ use crate::wire; use crate::{CallContext, CallError}; /// Permission request methods. +#[crate::async_trait] pub trait Permissions: Send + Sync { /// Request a device-capability permission from the user. /// diff --git a/rust/crates/truapi/src/api/preimage.rs b/rust/crates/truapi/src/api/preimage.rs index ad8769c9..5037f8cb 100644 --- a/rust/crates/truapi/src/api/preimage.rs +++ b/rust/crates/truapi/src/api/preimage.rs @@ -8,6 +8,7 @@ use crate::wire; use crate::{CallContext, CallError, Subscription}; /// Preimage lookup and submission methods. +#[crate::async_trait] pub trait Preimage: Send + Sync { /// Subscribe to preimage lookups for a given key. /// diff --git a/rust/crates/truapi/src/api/resource_allocation.rs b/rust/crates/truapi/src/api/resource_allocation.rs index 9d50a335..3de1003b 100644 --- a/rust/crates/truapi/src/api/resource_allocation.rs +++ b/rust/crates/truapi/src/api/resource_allocation.rs @@ -8,6 +8,7 @@ use crate::wire; use crate::{CallContext, CallError}; /// Resource pre-allocation (allowance management). +#[crate::async_trait] pub trait ResourceAllocation: Send + Sync { /// Request the host to pre-allocate one or more resources. /// diff --git a/rust/crates/truapi/src/api/signing.rs b/rust/crates/truapi/src/api/signing.rs index c2b44148..234d9a51 100644 --- a/rust/crates/truapi/src/api/signing.rs +++ b/rust/crates/truapi/src/api/signing.rs @@ -16,6 +16,7 @@ use crate::wire; use crate::{CallContext, CallError}; /// Signing operations. +#[crate::async_trait] pub trait Signing: Send + Sync { /// Construct a signed transaction for a product account. /// diff --git a/rust/crates/truapi/src/api/statement_store.rs b/rust/crates/truapi/src/api/statement_store.rs index 1110155c..38b16fb7 100644 --- a/rust/crates/truapi/src/api/statement_store.rs +++ b/rust/crates/truapi/src/api/statement_store.rs @@ -13,6 +13,7 @@ use crate::wire; use crate::{CallContext, CallError, Subscription}; /// Statement store methods. +#[crate::async_trait] pub trait StatementStore: Send + Sync { /// Subscribe to statements matching a topic filter. /// diff --git a/rust/crates/truapi/src/api/system.rs b/rust/crates/truapi/src/api/system.rs index 216cd45f..b08c5da9 100644 --- a/rust/crates/truapi/src/api/system.rs +++ b/rust/crates/truapi/src/api/system.rs @@ -10,6 +10,7 @@ use crate::{CallContext, CallError}; /// General-purpose TrUAPI methods for handshake, feature detection, /// and navigation. +#[crate::async_trait] pub trait System: Send + Sync { /// Negotiate the wire codec version with the product. /// diff --git a/rust/crates/truapi/src/api/theme.rs b/rust/crates/truapi/src/api/theme.rs index 5f78c3af..7fcbad81 100644 --- a/rust/crates/truapi/src/api/theme.rs +++ b/rust/crates/truapi/src/api/theme.rs @@ -5,6 +5,7 @@ use crate::wire; use crate::{CallContext, Subscription}; /// Host theme subscription. +#[crate::async_trait] pub trait Theme: Send + Sync { /// Subscribe to host theme changes. /// diff --git a/rust/crates/truapi/src/lib.rs b/rust/crates/truapi/src/lib.rs index a42fc689..5165ea55 100644 --- a/rust/crates/truapi/src/lib.rs +++ b/rust/crates/truapi/src/lib.rs @@ -2,8 +2,9 @@ //! //! Concrete wire types live in per-version modules. Versioned envelopes are in //! [`versioned`]. - -#![allow(async_fn_in_trait)] +//! Async API traits use the `async_trait` macro so their concise `async fn` methods +//! still guarantee `Send` futures. Implementations must annotate their impl +//! blocks with `#[truapi::async_trait]`. use core::convert::Infallible; use core::fmt; @@ -18,6 +19,8 @@ use std::sync::Mutex; use futures::Stream; use parity_scale_codec::{Decode, Encode}; +pub use async_trait::async_trait; + pub mod api; pub mod v01; pub mod versioned;