-
Notifications
You must be signed in to change notification settings - Fork 5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Multi-target API pain points #157
Comments
The last issue can be solved with the current API by placing I'll comment on the rest of the issues in a bit - for now I think this solves the majn pain point you mentioned in the chat. |
I remembered that Arcs exist, but I completely forgot that the send and receive datagram methods take a shared ref, so I don't have to wrap it in a Mutex. Yeah, putting the Session in an Arc works great and I'm just an idiot on that front 😄 I think the other issues are still valid though. I should be able to write a PR for the MTU one quickly - only question I have is what should it be called? |
This question often drives me into a multi-hour research and design iterations... But I guess we can start with this name, yeah. If you'll be adding it please also add the tests - sometging really simple yet that would break if the implementation changes in a way that breaks the API or logic. |
I think I have a somewhat elegant solution for JS errors in mind... In most cases you'd want the JS error there though. |
This is a known issue, and could be solved by providing a simpler crate that would join the wasm/native implementation together like at the example. However, the issue is that the API, really, is not just multi-target, it is also intended to be multi-implementation. Meaning, we want to be able to switch not just between native/wasm, but also between multiple native (and wasm) implementations. For instance, we currently offer just one native implementation - This design makes the Of course, in some cases your code can't be implemented in terms of only providing pre-made transports to the majority of the netcode, like when the transports have to be created on-the-fly, this gets more complicated - but when that complexity appears you usually see the direct access to the internals as a blessing; in this case, it allows specializing the transport setup code very flexibly. So, what am I saying with all this? In short:
To sum up: I don't mind having such API, but implementing it would most likely require a new crate that would depend on both In the mean time, I am back-and-forth designing with a type-erased wrapper around the arbitrary We already have a transparent wrapper This is the first part of my reply to the rest of the points brought up in this issue; I'll cover the rest later. So far, it has been very interesting, and I'm grateful for such feedback and an opportunity to share my thoughts on the concrete aspect of the design. |
The second, and the last topic what we didn't cover so far is this
I understand this is quite a difficult constraint, but in practice it should be possible to overcome. From a design point of view, we'd want the errors to contain the raw JS values so that it is possible to read the values from the inner JS error - as we don't really cover the API fully just yet, and there is useful data in those errors that can be fetched directly. Now, to the practical stuff - workarounds; with your example, you could do this: pub struct XwtError(pub String);
impl From<<internal::ClientEndpoint as Connect>::Error> for XwtError {
// extracts the error string from the xwt error
}
impl From<<internal::ClientEndpoint as Connect>::Connecting as xwt_core::endpoint::connect::Connecting>::Error> for XwtError {
// extracts the error string from the xwt error
}
pub enum ClientError {
/// Failed to connect to the target.
#[error("failed to connect")]
Connect(XwtError),
/// Failed to await the connection to the target.
#[error("failed to await connection")]
AwaitConnect(XwtError),
}
#[allow(clippy::useless_conversion)] // multi-target support
let conn = endpoint
.connect(&target)
.await
.map_err(XwtError::from)
.map_err(ClientError::Connect)?
.wait_connect()
.await
.map_err(XwtError::from)
.map_err(AwaitConnect)?; Still sort of annoying to write, but arguably better. With new traits though, it could even easier: pub struct XwtError(pub String);
impl From<<internal::ClientEndpoint as Connect>::Error> for XwtError {
// extracts the error string from the xwt error
}
impl From<<internal::ClientEndpoint as Connect>::Connecting as xwt_core::endpoint::connect::Connecting>::Error> for XwtError {
// extracts the error string from the xwt error
}
trait MapXwtErrorEx {
type Ok;
fn map_xwt_err<F: FnOnce(XwtError) -> U, U>(self, f: F) -> Result<Self::Ok, U>;
}
impl<T, E> MapXwtErrorEx for Result<T, E>
where
XwtError: From<E>,
{
type Ok = T;
fn map_xwt_err<F: FnOnce(XwtError) -> U, U>(self, f: F) -> Result<Self::Ok, U> {
self.map_err(XwtError::from).map_err(f)
}
}
pub enum ClientError {
/// Failed to connect to the target.
#[error("failed to connect")]
Connect(XwtError),
/// Failed to await the connection to the target.
#[error("failed to await connection")]
AwaitConnect(XwtError),
}
let conn = endpoint
.connect(&target)
.await
.map_xwt_err(ClientError::Connect)?
.wait_connect()
.await
.map_xwt_err(AwaitConnect)?; This is a bit of a hack, of course - but it almost improves the ergonomics to a reasonable level for now. Of course, we might want to, instead, figure out a way to represent those non-Send/non-Sync errors somehow; maybe an I am looking forward to help in this regard, and we should dedicate a whole new issue just for the work on this if we are going to do it. |
A lot of text, but there's no rush, so please take you time 😃 I hope it is useful. |
I appreciate the write-up! Let me give my thoughts on this:
I agree with your points, and although my own crates are targeting cross-platform instead of cross-implementation, I can understand why you want to target multiple implementations for the same platform. Because of this, I'm inclined to agree with just depending on For my own crate, I think this is enough justification to depend on the two implementations explicitly, and expose a For About Ultimately, I think the best solution for
Yeah I agree that there is useful data in the JS Error itself, and that users should be able to consume that raw error. But I don't think your workaround works on multi-platform: on WASM, the type ClientEndpoint = xwt_wtransport::Endpoint<endpoint_side::Client>;
type ConnectError = <ClientEndpoint as Connect>::Error; // or XwtError on WASM
type AwaitConnectError = <<ClientEndpoint as Connect>::Connecting as Connecting>::Error; // or XwtError on WASM The final error enum would look like: pub enum ClientError {
#[error("failed to connect")]
Connect(ConnectError),
#[error("failed to await connection")]
AwaitConnect(AwaitConnectError),
} I still do want to expose the raw error type on native, but just not on WASM, since that's I don't think an Exclusive or SyncWrapper would help here, since the error has to be Send as well, and Exclusive only provides Sync. Really, I think my old solution of |
Right now, I am having some problems with the developer experience when targeting multiple targets (wasm/native) with xwt. Even though the goal of xwt is to be cross-platform, it still feels like I have to use a lot of target-specific cfg blocks to get things done.
I'll add code examples of what my code looks like right now with target-specific cfgs, and what I would like it to look like ideally.
Initializing the endpoint in a cross-platform way is tricky
Ideally
Errors received from JS are not Send + Sync
Due to the
*mut u8
stored inside the JsValue. Arguably this is fine, since it is actually what is returned from the wasm-bindgen functions, but this seriously harms real-world usage. When I get a JsError, my (cross-platform) code does not care at all about the JsValue itself - it just wants a string representation of the error. So what I have to end up doing isWhen this could be made so much simpler for the end user:
At the cost of losing the raw JsValue, but remember - my code is cross-platform, I'm not even guaranteed to have a JsValue on native, so I have no use for it.
Getting MTU
There's no function on Session to get max datagram size. This one is just an oversight and can probably be fixed with a quick PR.
Handling the send/receive loop
On wtransport implementation, you are able to do code like
However, on WASM, this leads to weird behaviour where the transport can't receive as fast as it can send out. On WASM, you actually have to spawn the send/recv loops on separate
wasm_bindgen_futures
tasks. However, you can't split this singleConnection
up into its sending and receiving halves, so you can't spawn these tasks.This behaviour is also not documented anywhere and is basically just trial and error.
If possible, I would also like to retain the Connection or at least the
Rc<sys::WebTransport>
after splitting the connection into sender/receiver, so that I can also periodically get the MTU of the connection and send it over a channel.I would like code like this:
The text was updated successfully, but these errors were encountered: