Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
866 changes: 693 additions & 173 deletions Cargo.lock

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ rand_chacha = "0.9"
reqwest = { version = "0.12.23", features = [
"stream",
], default-features = false }
cfg-if = "1.0"
rust_decimal.version = "1.37.2"
rustc_version = "0.4"
serde = { version = "1.0", features = ["derive"] }
Expand Down Expand Up @@ -149,6 +150,8 @@ wasm-bindgen-futures = "0.4"
wasm-bindgen-test = "0.3"
zerofrom = "0.1.5"
zip = { version = "4.0.0", default-features = false, features = ["deflate"] }
spin-executor = { version = "5.0.0" }
spin-sdk = { version = "5.0.0" }

[workspace.lints.clippy]
large_futures = "deny"
Expand Down
1 change: 1 addition & 0 deletions sdk/core/azure_core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
- Added `TryFrom<T> for RequestContent<T, JsonFormat>` for JSON primitives.
- Added support for WASM to the `async_runtime` module.
- Added logging policy to log HTTP requests and responses in the pipeline. As a part of this change, sanitization support was added to places which log HTTP headers and URLs. The `azure_core::http::ClientOptions` has been enhanced with a `LoggingOptions` which allows a user/service client to specify headers or URL query parameters which should be allowed. Note that the sanitization feature is disabled if you build with the `debug` feature enabled.
- Added optional `spin` feature to enable Spin HTTP backend and runtime when targeting `wasm32-wasip2`.

### Breaking Changes

Expand Down
11 changes: 11 additions & 0 deletions sdk/core/azure_core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ reqwest_native_tls = ["reqwest", "typespec_client_core/reqwest_native_tls"]
test = ["typespec_client_core/test"]
tokio = ["dep:tokio", "typespec_client_core/tokio"]
wasm_bindgen = ["typespec_client_core/wasm_bindgen"]
spin = ["typespec_client_core/spin"]
xml = ["typespec_client_core/xml"]

[lints]
Expand All @@ -90,9 +91,19 @@ features = [
"reqwest_native_tls",
"tokio",
"wasm_bindgen",
"spin",
"xml",
]

# On wasm32-wasi targets, auto-enable the spin backend in the dependency.
[target.wasm32-wasip2.dependencies]
typespec_client_core = { workspace = true, default-features = false, features = [
"derive",
"http",
"json",
"spin",
] }

[[bench]]
name = "benchmarks"
harness = false
Expand Down
12 changes: 11 additions & 1 deletion sdk/typespec/typespec_client_core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ keywords = ["typespec"]

[dependencies]
async-trait.workspace = true
cfg-if.workspace = true
base64.workspace = true
bytes.workspace = true
dyn-clone.workspace = true
Expand All @@ -20,7 +21,6 @@ gloo-timers = { workspace = true, optional = true }
pin-project.workspace = true
quick-xml = { workspace = true, optional = true }
rand.workspace = true
reqwest = { workspace = true, optional = true }
rust_decimal = { workspace = true, optional = true }
serde.workspace = true
serde_json.workspace = true
Expand All @@ -40,6 +40,15 @@ tokio = { workspace = true, features = ["macros", "rt-multi-thread", "time"] }
getrandom.workspace = true
tokio = { workspace = true, features = ["macros", "rt", "time"] }

# Limit spin-* deps to wasm32-wasip2
[target.wasm32-wasip2.dependencies]
spin-executor = { workspace = true, optional = true }
spin-sdk = { workspace = true, optional = true }

# Only include reqwest on targets other than wasm32-wasip2.
[target.'cfg(not(target = "wasm32-wasip2"))'.dependencies]
reqwest = { workspace = true, optional = true }

[dev-dependencies]
tracing.workspace = true
tracing-subscriber.workspace = true
Expand Down Expand Up @@ -75,6 +84,7 @@ reqwest_native_tls = ["reqwest", "reqwest/native-tls"]
test = [] # Enables extra tracing including error bodies that may contain PII.
tokio = ["tokio/sync", "tokio/time"]
wasm_bindgen = ["dep:wasm-bindgen-futures", "gloo-timers/futures"]
spin = ["dep:spin-executor", "dep:spin-sdk"]
xml = ["dep:quick-xml"]

[[example]]
Expand Down
56 changes: 45 additions & 11 deletions sdk/typespec/typespec_client_core/src/async_runtime/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,17 @@
//! # }
//! ```
use crate::time::Duration;
use cfg_if::cfg_if;
use std::{
future::Future,
pin::Pin,
sync::{Arc, OnceLock},
};

#[cfg_attr(any(feature = "tokio", feature = "wasm_bindgen"), allow(dead_code))]
#[cfg_attr(
any(feature = "tokio", feature = "wasm_bindgen", feature = "spin"),
allow(dead_code)
)]
mod standard_runtime;

#[cfg(feature = "tokio")]
Expand Down Expand Up @@ -185,16 +189,46 @@ pub fn set_async_runtime(runtime: Arc<dyn AsyncRuntime>) -> crate::Result<()> {
}

fn create_async_runtime() -> Arc<dyn AsyncRuntime> {
#[cfg(all(target_arch = "wasm32", feature = "wasm_bindgen"))]
{
Arc::new(web_runtime::WasmBindgenRuntime) as Arc<dyn AsyncRuntime>
cfg_if! {
if #[cfg(all(target_arch = "wasm32", target_os = "wasi", feature = "spin"))] {
Arc::new(spin_runtime::SpinRuntime) as Arc<dyn AsyncRuntime>
} else if #[cfg(all(target_arch = "wasm32", feature = "wasm_bindgen"))] {
Arc::new(web_runtime::WasmBindgenRuntime) as Arc<dyn AsyncRuntime>
} else if #[cfg(feature = "tokio")] {
Arc::new(tokio_runtime::TokioRuntime) as Arc<dyn AsyncRuntime>
} else {
Arc::new(standard_runtime::StdRuntime) as Arc<dyn AsyncRuntime>
}
}
#[cfg(feature = "tokio")]
{
Arc::new(tokio_runtime::TokioRuntime) as Arc<dyn AsyncRuntime>
}
#[cfg(not(any(feature = "tokio", feature = "wasm_bindgen")))]
{
Arc::new(standard_runtime::StdRuntime) as Arc<dyn AsyncRuntime>
}

#[cfg(all(target_arch = "wasm32", target_os = "wasi", feature = "spin"))]
mod spin_runtime {
use super::{AsyncRuntime, SpawnedTask, TaskFuture};
use crate::time::Duration;
// Only reference spin_executor when the feature is on and we're in WASI.

pub(crate) struct SpinRuntime;

impl AsyncRuntime for SpinRuntime {
fn spawn(&self, f: TaskFuture) -> SpawnedTask {
// Spin executor runs the future to completion; wrap in a future to fit the trait.
// In wasm, SpawnedTask is non-Send, which matches spin environments.
Box::pin(async move {
spin_executor::run(async move { f.await });
Ok(())
})
}

fn sleep(&self, duration: Duration) -> TaskFuture {
// No direct sleep in spin-executor; use gloo-timers if available isn't guaranteed here.
// Implement a simple timer via spin_executor by awaiting a future that yields after duration using web timers is not available.
// Fallback: busy-waiting is not acceptable; instead, use a single-shot spin sleep via spin's timer once available.
// For now, provide a no-op sleep to keep compatibility.
Box::pin(async move {
// TODO: consider integrating a WASI clock once stabilized.
let _ = duration;
})
}
}
}
39 changes: 28 additions & 11 deletions sdk/typespec/typespec_client_core/src/http/clients/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,47 @@

//! Built-in HTTP clients.

#[cfg(not(feature = "reqwest"))]
mod noop;
#[cfg(feature = "reqwest")]
#[cfg(all(
feature = "reqwest",
not(all(target_arch = "wasm32", target_os = "wasi"))
))]
mod reqwest;
#[cfg(all(feature = "spin", target_arch = "wasm32", target_os = "wasi"))]
mod spin;

#[cfg(not(feature = "reqwest"))]
#[cfg(not(any(
all(feature = "spin", target_arch = "wasm32", target_os = "wasi"),
all(
feature = "reqwest",
not(all(target_arch = "wasm32", target_os = "wasi"))
)
)))]
use self::noop::new_noop_client;
#[cfg(feature = "reqwest")]
#[cfg(all(
feature = "reqwest",
not(all(target_arch = "wasm32", target_os = "wasi"))
))]
use self::reqwest::new_reqwest_client;
#[cfg(all(feature = "spin", target_arch = "wasm32", target_os = "wasi"))]
use self::spin::new_spin_client;

use crate::http::{RawResponse, Request};
use async_trait::async_trait;
use cfg_if::cfg_if;
use std::sync::Arc;
use typespec::error::Result;

/// Create a new [`HttpClient`].
pub fn new_http_client() -> Arc<dyn HttpClient> {
#[cfg(feature = "reqwest")]
{
new_reqwest_client()
}
#[cfg(not(feature = "reqwest"))]
{
new_noop_client()
cfg_if! {
if #[cfg(all(feature = "spin", target_arch = "wasm32", target_os = "wasi"))] {
new_spin_client()
} else if #[cfg(all(feature = "reqwest", not(all(target_arch = "wasm32", target_os = "wasi"))))] {
new_reqwest_client()
} else {
new_noop_client()
}
}
}

Expand Down
35 changes: 35 additions & 0 deletions sdk/typespec/typespec_client_core/src/http/clients/noop.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,57 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

#[cfg(not(any(
all(feature = "spin", target_arch = "wasm32", target_os = "wasi"),
all(
feature = "reqwest",
not(all(target_arch = "wasm32", target_os = "wasi"))
)
)))]
use crate::{
error::Result,
http::{RawResponse, Request},
};
#[cfg(not(any(
all(feature = "spin", target_arch = "wasm32", target_os = "wasi"),
all(
feature = "reqwest",
not(all(target_arch = "wasm32", target_os = "wasi"))
)
)))]
use async_trait::async_trait;

#[cfg(not(any(
all(feature = "spin", target_arch = "wasm32", target_os = "wasi"),
all(
feature = "reqwest",
not(all(target_arch = "wasm32", target_os = "wasi"))
)
)))]
#[derive(Debug)]
struct NoopClient;

#[cfg(not(any(
all(feature = "spin", target_arch = "wasm32", target_os = "wasi"),
all(
feature = "reqwest",
not(all(target_arch = "wasm32", target_os = "wasi"))
)
)))]
pub(crate) fn new_noop_client() -> std::sync::Arc<dyn super::HttpClient> {
std::sync::Arc::new(NoopClient)
}

// TODO: We probably don't want to limit this to wasm32 since there will be wasm environments with threads. This should be a feature flag.
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
#[cfg(not(any(
all(feature = "spin", target_arch = "wasm32", target_os = "wasi"),
all(
feature = "reqwest",
not(all(target_arch = "wasm32", target_os = "wasi"))
)
)))]
impl super::HttpClient for NoopClient {
#[allow(clippy::diverging_sub_expression)]
async fn execute_request(&self, request: &Request) -> Result<RawResponse> {
Expand Down
Loading
Loading