gpui_web: Support Fetch requests from background workers (#61782)
Browser Fetch APIs must run on the main thread, while GPUI image loading can invoke its HTTP client from background WASM workers. This routes `FetchHttpClient` requests through the web dispatcher's main-thread mailbox, installs the client by default for web applications, and removes thread-affine browser state from the safely movable dispatcher. Release Notes: - N/A
This commit is contained in:
1 parent
dbd8acd97b
commit
182babebf0
6 files changed
+198
-195
No files matched your search
@@ -164,16 +164,6 @@ fn run_example() {
|
||||
let http_client = ReqwestClient::user_agent("gpui example").unwrap();
|
||||
cx.set_http_client(Arc::new(http_client));
|
||||
}
|
||||
#[cfg(target_family = "wasm")]
|
||||
{
|
||||
// Safety: the web examples run single-threaded; the client is
|
||||
// created and used exclusively on the main thread.
|
||||
let http_client = unsafe {
|
||||
gpui_web::FetchHttpClient::with_user_agent("gpui example")
|
||||
.expect("failed to create FetchHttpClient")
|
||||
};
|
||||
cx.set_http_client(Arc::new(http_client));
|
||||
}
|
||||
|
||||
cx.activate(true);
|
||||
cx.on_action(|_: &Quit, cx| cx.quit());
|
||||
|
||||
@@ -259,16 +259,6 @@ fn run_example() {
|
||||
let http_client = ReqwestClient::user_agent("gpui example").unwrap();
|
||||
cx.set_http_client(Arc::new(http_client));
|
||||
}
|
||||
#[cfg(target_family = "wasm")]
|
||||
{
|
||||
// Safety: the web examples run single-threaded; the client is
|
||||
// created and used exclusively on the main thread.
|
||||
let http_client = unsafe {
|
||||
gpui_web::FetchHttpClient::with_user_agent("gpui example")
|
||||
.expect("failed to create FetchHttpClient")
|
||||
};
|
||||
cx.set_http_client(Arc::new(http_client));
|
||||
}
|
||||
|
||||
cx.activate(true);
|
||||
cx.on_action(|_: &Quit, cx| cx.quit());
|
||||
|
||||
@@ -11,6 +11,14 @@ pub fn background_executor() -> gpui::BackgroundExecutor {
|
||||
}
|
||||
|
||||
pub fn application() -> gpui::Application {
|
||||
#[cfg(target_family = "wasm")]
|
||||
{
|
||||
let platform = Rc::new(gpui_web::WebPlatform::new(true));
|
||||
let http_client = std::sync::Arc::new(platform.fetch_http_client());
|
||||
gpui::Application::with_platform(platform).with_http_client(http_client)
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
gpui::Application::with_platform(current_platform(false))
|
||||
}
|
||||
|
||||
@@ -21,7 +29,9 @@ pub fn headless() -> gpui::Application {
|
||||
/// Unlike `application`, this function returns a single-threaded web application.
|
||||
#[cfg(target_family = "wasm")]
|
||||
pub fn single_threaded_web() -> gpui::Application {
|
||||
gpui::Application::with_platform(Rc::new(gpui_web::WebPlatform::new(false)))
|
||||
let platform = Rc::new(gpui_web::WebPlatform::new(false));
|
||||
let http_client = std::sync::Arc::new(platform.fetch_http_client());
|
||||
gpui::Application::with_platform(platform).with_http_client(http_client)
|
||||
}
|
||||
|
||||
/// Initializes panic hooks and logging for the web platform.
|
||||
|
||||
@@ -21,12 +21,25 @@ fn shared_memory_supported() -> bool {
|
||||
has_shared_array_buffer && has_atomics && is_shared_buffer
|
||||
}
|
||||
|
||||
fn wait_async_supported() -> bool {
|
||||
let global = js_sys::global();
|
||||
let Ok(atomics) = js_sys::Reflect::get(&global, &JsValue::from_str("Atomics")) else {
|
||||
return false;
|
||||
};
|
||||
let Ok(wait_async) = js_sys::Reflect::get(&atomics, &JsValue::from_str("waitAsync")) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
wait_async.is_function()
|
||||
}
|
||||
|
||||
enum MainThreadItem {
|
||||
Runnable(RunnableVariant),
|
||||
Delayed {
|
||||
runnable: RunnableVariant,
|
||||
millis: i32,
|
||||
},
|
||||
Function(Box<dyn FnOnce() + Send>),
|
||||
// TODO-Wasm: Shouldn't these run on their own dedicated thread?
|
||||
RealtimeFunction(Box<dyn FnOnce() + Send>),
|
||||
}
|
||||
@@ -127,7 +140,6 @@ impl MainThreadMailbox {
|
||||
|
||||
pub struct WebDispatcher {
|
||||
main_thread_id: std::thread::ThreadId,
|
||||
browser_window: web_sys::Window,
|
||||
background_sender: PriorityQueueSender<RunnableVariant>,
|
||||
main_thread_mailbox: Arc<MainThreadMailbox>,
|
||||
supports_threads: bool,
|
||||
@@ -135,11 +147,6 @@ pub struct WebDispatcher {
|
||||
_background_threads: Vec<wasm_thread::JoinHandle<()>>,
|
||||
}
|
||||
|
||||
// Safety: `web_sys::Window` is only accessed from the main thread
|
||||
// All other fields are `Send + Sync` by construction.
|
||||
unsafe impl Send for WebDispatcher {}
|
||||
unsafe impl Sync for WebDispatcher {}
|
||||
|
||||
impl WebDispatcher {
|
||||
pub fn new(browser_window: web_sys::Window, allow_threads: bool) -> Self {
|
||||
#[cfg(feature = "multithreaded")]
|
||||
@@ -149,14 +156,16 @@ impl WebDispatcher {
|
||||
|
||||
let main_thread_mailbox = Arc::new(MainThreadMailbox::new());
|
||||
|
||||
let supports_threads =
|
||||
cfg!(feature = "multithreaded") && allow_threads && shared_memory_supported();
|
||||
let supports_threads = cfg!(feature = "multithreaded")
|
||||
&& allow_threads
|
||||
&& shared_memory_supported()
|
||||
&& wait_async_supported();
|
||||
|
||||
if supports_threads {
|
||||
main_thread_mailbox.run_waker_loop(browser_window.clone());
|
||||
} else if cfg!(feature = "multithreaded") && allow_threads {
|
||||
log::warn!(
|
||||
"SharedArrayBuffer not available; falling back to single-threaded dispatcher"
|
||||
"Required WebAssembly threading APIs are unavailable; falling back to single-threaded dispatcher"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -197,7 +206,6 @@ impl WebDispatcher {
|
||||
|
||||
Self {
|
||||
main_thread_id: std::thread::current().id(),
|
||||
browser_window,
|
||||
background_sender,
|
||||
main_thread_mailbox,
|
||||
supports_threads,
|
||||
@@ -209,6 +217,19 @@ impl WebDispatcher {
|
||||
fn on_main_thread(&self) -> bool {
|
||||
std::thread::current().id() == self.main_thread_id
|
||||
}
|
||||
|
||||
pub(crate) fn dispatch_function_on_main_thread(
|
||||
&self,
|
||||
function: impl FnOnce() + Send + 'static,
|
||||
) {
|
||||
if self.on_main_thread() {
|
||||
let callback = Closure::once_into_js(function);
|
||||
browser_window().queue_microtask(callback.unchecked_ref());
|
||||
} else {
|
||||
self.main_thread_mailbox
|
||||
.post(Priority::High, MainThreadItem::Function(Box::new(function)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PlatformDispatcher for WebDispatcher {
|
||||
@@ -235,7 +256,7 @@ impl PlatformDispatcher for WebDispatcher {
|
||||
|
||||
fn dispatch_on_main_thread(&self, runnable: RunnableVariant, priority: Priority) {
|
||||
if self.on_main_thread() {
|
||||
schedule_runnable(&self.browser_window, runnable, priority);
|
||||
schedule_runnable(&browser_window(), runnable, priority);
|
||||
} else {
|
||||
self.main_thread_mailbox
|
||||
.post(priority, MainThreadItem::Runnable(runnable));
|
||||
@@ -248,7 +269,7 @@ impl PlatformDispatcher for WebDispatcher {
|
||||
let callback = Closure::once_into_js(move || {
|
||||
runnable.run();
|
||||
});
|
||||
self.browser_window
|
||||
browser_window()
|
||||
.set_timeout_with_callback_and_timeout_and_arguments_0(
|
||||
callback.unchecked_ref(),
|
||||
millis,
|
||||
@@ -265,8 +286,7 @@ impl PlatformDispatcher for WebDispatcher {
|
||||
let callback = Closure::once_into_js(move || {
|
||||
function();
|
||||
});
|
||||
self.browser_window
|
||||
.queue_microtask(callback.unchecked_ref());
|
||||
browser_window().queue_microtask(callback.unchecked_ref());
|
||||
} else {
|
||||
self.main_thread_mailbox
|
||||
.post(Priority::High, MainThreadItem::RealtimeFunction(function));
|
||||
@@ -278,6 +298,10 @@ impl PlatformDispatcher for WebDispatcher {
|
||||
}
|
||||
}
|
||||
|
||||
fn browser_window() -> web_sys::Window {
|
||||
web_sys::window().expect("must be running in a browser window context")
|
||||
}
|
||||
|
||||
fn execute_on_main_thread(window: &web_sys::Window, item: MainThreadItem) {
|
||||
match item {
|
||||
MainThreadItem::Runnable(runnable) => {
|
||||
@@ -294,7 +318,7 @@ fn execute_on_main_thread(window: &web_sys::Window, item: MainThreadItem) {
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
MainThreadItem::RealtimeFunction(function) => {
|
||||
MainThreadItem::Function(function) | MainThreadItem::RealtimeFunction(function) => {
|
||||
function();
|
||||
}
|
||||
}
|
||||
|
||||
+131
-158
@@ -1,9 +1,8 @@
|
||||
use anyhow::anyhow;
|
||||
use futures::AsyncReadExt as _;
|
||||
use crate::WebDispatcher;
|
||||
use anyhow::{Context as _, anyhow};
|
||||
use futures::{AsyncReadExt as _, channel::oneshot};
|
||||
use http_client::{AsyncBody, HttpClient, RedirectPolicy};
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::task::Poll;
|
||||
use std::sync::Arc;
|
||||
use wasm_bindgen::JsCast as _;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
@@ -14,6 +13,7 @@ extern "C" {
|
||||
}
|
||||
|
||||
pub struct FetchHttpClient {
|
||||
dispatcher: Arc<WebDispatcher>,
|
||||
user_agent: Option<http_client::http::header::HeaderValue>,
|
||||
credentials: FetchCredentials,
|
||||
}
|
||||
@@ -31,79 +31,33 @@ pub enum FetchCredentials {
|
||||
}
|
||||
|
||||
impl FetchHttpClient {
|
||||
// Kept private so that all construction goes through the constructors
|
||||
// below, which are `unsafe` when background threads are enabled.
|
||||
fn build(user_agent: Option<http_client::http::header::HeaderValue>) -> Self {
|
||||
pub(crate) fn new(dispatcher: Arc<WebDispatcher>) -> Self {
|
||||
Self {
|
||||
user_agent,
|
||||
dispatcher,
|
||||
user_agent: None,
|
||||
credentials: FetchCredentials::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn with_user_agent(
|
||||
dispatcher: Arc<WebDispatcher>,
|
||||
user_agent: &str,
|
||||
) -> anyhow::Result<Self> {
|
||||
Ok(Self {
|
||||
dispatcher,
|
||||
user_agent: Some(http_client::http::header::HeaderValue::from_str(
|
||||
user_agent,
|
||||
)?),
|
||||
credentials: FetchCredentials::default(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn with_credentials(mut self, credentials: FetchCredentials) -> Self {
|
||||
self.credentials = credentials;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "multithreaded")]
|
||||
impl FetchHttpClient {
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller must ensure the client is only used from the main thread:
|
||||
/// the futures returned by [`HttpClient::send`] call browser APIs that do
|
||||
/// not exist on worker threads, so they must never be polled from a task
|
||||
/// spawned on the background executor.
|
||||
pub unsafe fn new() -> Self {
|
||||
Self::build(None)
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller must ensure the client is only used from the main thread:
|
||||
/// the futures returned by [`HttpClient::send`] call browser APIs that do
|
||||
/// not exist on worker threads, so they must never be polled from a task
|
||||
/// spawned on the background executor.
|
||||
pub unsafe fn with_user_agent(user_agent: &str) -> anyhow::Result<Self> {
|
||||
Ok(Self::build(Some(
|
||||
http_client::http::header::HeaderValue::from_str(user_agent)?,
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "multithreaded"))]
|
||||
impl FetchHttpClient {
|
||||
pub fn new() -> Self {
|
||||
Self::build(None)
|
||||
}
|
||||
|
||||
pub fn with_user_agent(user_agent: &str) -> anyhow::Result<Self> {
|
||||
Ok(Self::build(Some(
|
||||
http_client::http::header::HeaderValue::from_str(user_agent)?,
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Wraps a `!Send` future to satisfy the `Send` bound on `BoxFuture`.
|
||||
///
|
||||
/// Safety: only valid because the `FetchHttpClient` constructors confine the
|
||||
/// client to the main thread (via their `unsafe` contract when
|
||||
/// `multithreaded` is enabled, or by the absence of threads when it is not),
|
||||
/// so the wrapped future is never actually sent across threads.
|
||||
struct AssertSend<F>(F);
|
||||
|
||||
unsafe impl<F> Send for AssertSend<F> {}
|
||||
|
||||
impl<F: Future> Future for AssertSend<F> {
|
||||
type Output = F::Output;
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll<Self::Output> {
|
||||
// Safety: pin projection for a single-field newtype wrapper.
|
||||
let inner = unsafe { self.map_unchecked_mut(|this| &mut this.0) };
|
||||
inner.poll(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl HttpClient for FetchHttpClient {
|
||||
fn user_agent(&self) -> Option<&http_client::http::header::HeaderValue> {
|
||||
self.user_agent.as_ref()
|
||||
@@ -120,105 +74,124 @@ impl HttpClient for FetchHttpClient {
|
||||
{
|
||||
let (parts, body) = req.into_parts();
|
||||
let credentials = self.credentials;
|
||||
let dispatcher = self.dispatcher.clone();
|
||||
|
||||
Box::pin(AssertSend(async move {
|
||||
Box::pin(async move {
|
||||
let body_bytes = read_body_to_bytes(body).await?;
|
||||
let (sender, receiver) = oneshot::channel();
|
||||
|
||||
let init = web_sys::RequestInit::new();
|
||||
init.set_method(parts.method.as_str());
|
||||
init.set_credentials(match credentials {
|
||||
FetchCredentials::Omit => web_sys::RequestCredentials::Omit,
|
||||
FetchCredentials::SameOrigin => web_sys::RequestCredentials::SameOrigin,
|
||||
FetchCredentials::Include => web_sys::RequestCredentials::Include,
|
||||
dispatcher.dispatch_function_on_main_thread(move || {
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
let result = fetch(parts, body_bytes, credentials).await;
|
||||
if sender.send(result).is_err() {
|
||||
log::debug!("fetch response receiver was dropped");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if let Some(redirect_policy) = parts.extensions.get::<RedirectPolicy>() {
|
||||
match redirect_policy {
|
||||
RedirectPolicy::NoFollow => {
|
||||
init.set_redirect(web_sys::RequestRedirect::Manual);
|
||||
}
|
||||
RedirectPolicy::FollowLimit(_) | RedirectPolicy::FollowAll => {
|
||||
init.set_redirect(web_sys::RequestRedirect::Follow);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref bytes) = body_bytes {
|
||||
let uint8array = js_sys::Uint8Array::from(bytes.as_slice());
|
||||
init.set_body(uint8array.as_ref());
|
||||
}
|
||||
|
||||
let url = parts.uri.to_string();
|
||||
let request = web_sys::Request::new_with_str_and_init(&url, &init)
|
||||
.map_err(|error| anyhow!("failed to create fetch Request: {error:?}"))?;
|
||||
|
||||
let request_headers = request.headers();
|
||||
for (name, value) in &parts.headers {
|
||||
let value_str = value
|
||||
.to_str()
|
||||
.map_err(|_| anyhow!("non-ASCII header value for {name}"))?;
|
||||
request_headers
|
||||
.set(name.as_str(), value_str)
|
||||
.map_err(|error| anyhow!("failed to set header {name}: {error:?}"))?;
|
||||
}
|
||||
|
||||
let promise = global_fetch(&request)
|
||||
.map_err(|error| anyhow!("fetch threw an error: {error:?}"))?;
|
||||
let response_value = wasm_bindgen_futures::JsFuture::from(promise)
|
||||
.await
|
||||
.map_err(|error| anyhow!("fetch failed: {error:?}"))?;
|
||||
|
||||
let web_response: web_sys::Response = response_value
|
||||
.dyn_into()
|
||||
.map_err(|error| anyhow!("fetch result is not a Response: {error:?}"))?;
|
||||
|
||||
let status = web_response.status();
|
||||
let mut builder = http_client::http::Response::builder().status(status);
|
||||
|
||||
// `Headers` is a JS iterable yielding `[name, value]` pairs.
|
||||
// `js_sys::Array::from` calls `Array.from()` which accepts any iterable.
|
||||
let header_pairs = js_sys::Array::from(&web_response.headers());
|
||||
for index in 0..header_pairs.length() {
|
||||
match header_pairs.get(index).dyn_into::<js_sys::Array>() {
|
||||
Ok(pair) => match (pair.get(0).as_string(), pair.get(1).as_string()) {
|
||||
(Some(name), Some(value)) => {
|
||||
builder = builder.header(name, value);
|
||||
}
|
||||
(name, value) => {
|
||||
log::warn!(
|
||||
"skipping response header at index {index}: \
|
||||
name={name:?}, value={value:?}"
|
||||
);
|
||||
}
|
||||
},
|
||||
Err(entry) => {
|
||||
log::warn!("skipping non-array header entry at index {index}: {entry:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The entire response body is eagerly buffered into memory via
|
||||
// `arrayBuffer()`. The Fetch API does not expose a synchronous
|
||||
// streaming interface; streaming would require `ReadableStream`
|
||||
// interop which is significantly more complex.
|
||||
let body_promise = web_response
|
||||
.array_buffer()
|
||||
.map_err(|error| anyhow!("failed to initiate response body read: {error:?}"))?;
|
||||
let body_value = wasm_bindgen_futures::JsFuture::from(body_promise)
|
||||
.await
|
||||
.map_err(|error| anyhow!("failed to read response body: {error:?}"))?;
|
||||
let array_buffer: js_sys::ArrayBuffer = body_value
|
||||
.dyn_into()
|
||||
.map_err(|error| anyhow!("response body is not an ArrayBuffer: {error:?}"))?;
|
||||
let response_bytes = js_sys::Uint8Array::new(&array_buffer).to_vec();
|
||||
|
||||
builder
|
||||
.body(AsyncBody::from(response_bytes))
|
||||
.map_err(|error| anyhow!(error))
|
||||
}))
|
||||
receiver.await.context("browser fetch task was canceled")?
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn fetch(
|
||||
parts: http_client::http::request::Parts,
|
||||
body_bytes: Option<Vec<u8>>,
|
||||
credentials: FetchCredentials,
|
||||
) -> anyhow::Result<http_client::http::Response<AsyncBody>> {
|
||||
let init = web_sys::RequestInit::new();
|
||||
init.set_method(parts.method.as_str());
|
||||
init.set_credentials(match credentials {
|
||||
FetchCredentials::Omit => web_sys::RequestCredentials::Omit,
|
||||
FetchCredentials::SameOrigin => web_sys::RequestCredentials::SameOrigin,
|
||||
FetchCredentials::Include => web_sys::RequestCredentials::Include,
|
||||
});
|
||||
|
||||
if let Some(redirect_policy) = parts.extensions.get::<RedirectPolicy>() {
|
||||
match redirect_policy {
|
||||
RedirectPolicy::NoFollow => {
|
||||
init.set_redirect(web_sys::RequestRedirect::Manual);
|
||||
}
|
||||
RedirectPolicy::FollowLimit(_) | RedirectPolicy::FollowAll => {
|
||||
init.set_redirect(web_sys::RequestRedirect::Follow);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref bytes) = body_bytes {
|
||||
let uint8array = js_sys::Uint8Array::from(bytes.as_slice());
|
||||
init.set_body(uint8array.as_ref());
|
||||
}
|
||||
|
||||
let url = parts.uri.to_string();
|
||||
let request = web_sys::Request::new_with_str_and_init(&url, &init)
|
||||
.map_err(|error| anyhow!("failed to create fetch Request: {error:?}"))?;
|
||||
|
||||
let request_headers = request.headers();
|
||||
for (name, value) in &parts.headers {
|
||||
let value_str = value
|
||||
.to_str()
|
||||
.map_err(|_| anyhow!("non-ASCII header value for {name}"))?;
|
||||
request_headers
|
||||
.set(name.as_str(), value_str)
|
||||
.map_err(|error| anyhow!("failed to set header {name}: {error:?}"))?;
|
||||
}
|
||||
|
||||
let promise =
|
||||
global_fetch(&request).map_err(|error| anyhow!("fetch threw an error: {error:?}"))?;
|
||||
let response_value = wasm_bindgen_futures::JsFuture::from(promise)
|
||||
.await
|
||||
.map_err(|error| anyhow!("fetch failed: {error:?}"))?;
|
||||
|
||||
let web_response: web_sys::Response = response_value
|
||||
.dyn_into()
|
||||
.map_err(|error| anyhow!("fetch result is not a Response: {error:?}"))?;
|
||||
|
||||
let status = web_response.status();
|
||||
let mut builder = http_client::http::Response::builder().status(status);
|
||||
|
||||
// `Headers` is a JS iterable yielding `[name, value]` pairs.
|
||||
// `js_sys::Array::from` calls `Array.from()` which accepts any iterable.
|
||||
let header_pairs = js_sys::Array::from(&web_response.headers());
|
||||
for index in 0..header_pairs.length() {
|
||||
match header_pairs.get(index).dyn_into::<js_sys::Array>() {
|
||||
Ok(pair) => match (pair.get(0).as_string(), pair.get(1).as_string()) {
|
||||
(Some(name), Some(value)) => {
|
||||
builder = builder.header(name, value);
|
||||
}
|
||||
(name, value) => {
|
||||
log::warn!(
|
||||
"skipping response header at index {index}: \
|
||||
name={name:?}, value={value:?}"
|
||||
);
|
||||
}
|
||||
},
|
||||
Err(entry) => {
|
||||
log::warn!("skipping non-array header entry at index {index}: {entry:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The entire response body is eagerly buffered into memory via
|
||||
// `arrayBuffer()`. The Fetch API does not expose a synchronous
|
||||
// streaming interface; streaming would require `ReadableStream`
|
||||
// interop which is significantly more complex.
|
||||
let body_promise = web_response
|
||||
.array_buffer()
|
||||
.map_err(|error| anyhow!("failed to initiate response body read: {error:?}"))?;
|
||||
let body_value = wasm_bindgen_futures::JsFuture::from(body_promise)
|
||||
.await
|
||||
.map_err(|error| anyhow!("failed to read response body: {error:?}"))?;
|
||||
let array_buffer: js_sys::ArrayBuffer = body_value
|
||||
.dyn_into()
|
||||
.map_err(|error| anyhow!("response body is not an ArrayBuffer: {error:?}"))?;
|
||||
let response_bytes = js_sys::Uint8Array::new(&array_buffer).to_vec();
|
||||
|
||||
builder
|
||||
.body(AsyncBody::from(response_bytes))
|
||||
.map_err(|error| anyhow!(error))
|
||||
}
|
||||
|
||||
async fn read_body_to_bytes(mut body: AsyncBody) -> anyhow::Result<Option<Vec<u8>>> {
|
||||
let mut buffer = Vec::new();
|
||||
body.read_to_end(&mut buffer).await?;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::dispatcher::WebDispatcher;
|
||||
use crate::display::WebDisplay;
|
||||
use crate::events::EventListenerHandle;
|
||||
use crate::http_client::FetchHttpClient;
|
||||
use crate::keyboard::WebKeyboardLayout;
|
||||
use crate::window::WebWindow;
|
||||
use anyhow::Result;
|
||||
@@ -34,6 +35,7 @@ static BUNDLED_FONTS: &[&[u8]] = &[
|
||||
|
||||
pub struct WebPlatform {
|
||||
browser_window: web_sys::Window,
|
||||
dispatcher: Arc<WebDispatcher>,
|
||||
background_executor: BackgroundExecutor,
|
||||
foreground_executor: ForegroundExecutor,
|
||||
text_system: Arc<dyn PlatformTextSystem>,
|
||||
@@ -67,7 +69,7 @@ impl WebPlatform {
|
||||
allow_multi_threading,
|
||||
));
|
||||
let background_executor = BackgroundExecutor::new(dispatcher.clone());
|
||||
let foreground_executor = ForegroundExecutor::new(dispatcher);
|
||||
let foreground_executor = ForegroundExecutor::new(dispatcher.clone());
|
||||
let text_system = Arc::new(gpui_wgpu::CosmicTextSystem::new_without_system_fonts(
|
||||
"IBM Plex Sans",
|
||||
));
|
||||
@@ -92,6 +94,7 @@ impl WebPlatform {
|
||||
|
||||
Self {
|
||||
browser_window,
|
||||
dispatcher,
|
||||
background_executor,
|
||||
foreground_executor,
|
||||
text_system,
|
||||
@@ -104,6 +107,19 @@ impl WebPlatform {
|
||||
_cursor_restore_listeners: cursor_restore_listeners,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns an HTTP client that runs browser Fetch operations on this platform's main thread.
|
||||
pub fn fetch_http_client(&self) -> FetchHttpClient {
|
||||
FetchHttpClient::new(self.dispatcher.clone())
|
||||
}
|
||||
|
||||
/// Returns a browser Fetch HTTP client with the given reported user agent.
|
||||
pub fn fetch_http_client_with_user_agent(
|
||||
&self,
|
||||
user_agent: &str,
|
||||
) -> anyhow::Result<FetchHttpClient> {
|
||||
FetchHttpClient::with_user_agent(self.dispatcher.clone(), user_agent)
|
||||
}
|
||||
}
|
||||
|
||||
impl Platform for WebPlatform {
|
||||
|
||||
Reference in new issue
Block a user