Skip to content

Commit f9472f4

Browse files
committed
feat: Interrupt the last image loading request
1 parent 3f70869 commit f9472f4

File tree

7 files changed

+169
-39
lines changed

7 files changed

+169
-39
lines changed

packages/blitz-dom/src/document.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1304,7 +1304,7 @@ impl BaseDocument {
13041304

13051305
/// Used to determine whether a document matches a media query string,
13061306
/// and to monitor a document to detect when it matches (or stops matching) that media query.
1307-
///
1307+
///
13081308
/// https://developer.mozilla.org/en-US/docs/Web/API/Window/matchMedia
13091309
pub fn match_media(&self, media_query_string: &str) -> bool {
13101310
let mut input = cssparser::ParserInput::new(media_query_string);

packages/blitz-dom/src/layout/construct.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -108,10 +108,11 @@ pub(crate) fn collect_layout_children(
108108
.unwrap()
109109
.element_data_mut()
110110
.unwrap()
111-
.node_specific_data = NodeSpecificData::Image(Box::new(ImageContext {
112-
selected_source: ImageSource::new("about:blank".to_string()),
113-
data: Some(svg.into()),
114-
}));
111+
.node_specific_data =
112+
NodeSpecificData::Image(Box::new(ImageContext::new_with_data(
113+
ImageSource::new("about:blank".to_string()),
114+
svg.into(),
115+
)));
115116
}
116117
Err(err) => {
117118
println!("{container_node_id} SVG parse failed");

packages/blitz-dom/src/node.rs

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use atomic_refcell::{AtomicRef, AtomicRefCell};
2+
use blitz_traits::net::AbortController;
23
use color::{AlphaColor, Srgb};
34
use keyboard_types::Modifiers;
45
use markup5ever::{LocalName, QualName, local_name};
@@ -584,17 +585,37 @@ impl From<usvg::Tree> for ImageData {
584585
}
585586
}
586587

587-
#[derive(Clone)]
588+
#[derive(Debug)]
588589
pub struct ImageContext {
589590
pub selected_source: ImageSource,
590591
pub data: Option<ImageData>,
592+
pub controller: Option<AbortController>,
593+
}
594+
595+
impl Clone for ImageContext {
596+
fn clone(&self) -> Self {
597+
Self {
598+
selected_source: self.selected_source.clone(),
599+
data: self.data.clone(),
600+
controller: None,
601+
}
602+
}
591603
}
592604

593605
impl ImageContext {
594-
fn new(selected_source: ImageSource) -> Self {
606+
fn new_with_controller(selected_source: ImageSource, controller: AbortController) -> Self {
595607
Self {
596608
selected_source,
597609
data: None,
610+
controller: Some(controller),
611+
}
612+
}
613+
614+
pub fn new_with_data(selected_source: ImageSource, data: ImageData) -> Self {
615+
Self {
616+
selected_source,
617+
data: Some(data),
618+
controller: None,
598619
}
599620
}
600621
}

packages/blitz-dom/src/node/image.rs

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use super::{ImageContext, NodeSpecificData};
22
use crate::{BaseDocument, net::ImageHandler, util::ImageType};
3-
use blitz_traits::net::Request;
3+
use blitz_traits::net::{AbortController, Request};
44
use markup5ever::local_name;
55
use mime::Mime;
66
use std::{
@@ -44,23 +44,31 @@ impl BaseDocument {
4444
return;
4545
};
4646

47-
if let NodeSpecificData::Image(context) = &mut data.node_specific_data {
48-
if context.selected_source.url == selected_source.url
49-
&& context.selected_source.descriptor.density == selected_source.descriptor.density
47+
let controller = AbortController::default();
48+
let signal = controller.signal.clone();
49+
50+
match &mut data.node_specific_data {
51+
NodeSpecificData::Image(context)
52+
if !context
53+
.selected_source
54+
.is_same_image_source(&selected_source) =>
5055
{
51-
return;
52-
} else {
5356
context.selected_source = selected_source;
57+
if let Some(controller) = context.controller.replace(controller) {
58+
controller.abort();
59+
}
5460
}
55-
} else if let NodeSpecificData::None = data.node_specific_data {
56-
data.node_specific_data =
57-
NodeSpecificData::Image(Box::new(ImageContext::new(selected_source)));
58-
} else {
59-
return;
61+
NodeSpecificData::None => {
62+
data.node_specific_data = NodeSpecificData::Image(Box::new(
63+
ImageContext::new_with_controller(selected_source, controller),
64+
));
65+
}
66+
_ => return,
6067
}
68+
6169
self.net_provider.fetch(
6270
self.id(),
63-
Request::get(src),
71+
Request::get(src).signal(signal),
6472
Box::new(ImageHandler::new(target_id, ImageType::Image)),
6573
);
6674
}
@@ -383,6 +391,11 @@ impl ImageSource {
383391
}
384392
}
385393

394+
#[inline]
395+
fn is_same_image_source(&self, other: &Self) -> bool {
396+
self.url == other.url && self.descriptor.density == other.descriptor.density
397+
}
398+
386399
fn parse(input: &str) -> Option<Self> {
387400
let image_source_split = input.split_ascii_whitespace().collect::<Vec<&str>>();
388401
let len = image_source_split.len();

packages/blitz-net/src/lib.rs

Lines changed: 63 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
use blitz_traits::net::{BoxedHandler, Bytes, NetCallback, NetProvider, Request, SharedCallback};
1+
use blitz_traits::net::{
2+
AbortSignal, BoxedHandler, Bytes, NetCallback, NetProvider, Request, SharedCallback,
3+
};
24
use data_url::DataUrl;
35
use reqwest::Client;
4-
use std::sync::Arc;
6+
use std::{marker::PhantomData, pin::Pin, sync::Arc, task::Poll};
57
use tokio::{
68
runtime::Handle,
79
sync::mpsc::{UnboundedReceiver, UnboundedSender, unbounded_channel},
@@ -75,18 +77,6 @@ impl<D: 'static> Provider<D> {
7577
})
7678
}
7779

78-
async fn fetch_with_handler(
79-
client: Client,
80-
doc_id: usize,
81-
request: Request,
82-
handler: BoxedHandler<D>,
83-
res_callback: SharedCallback<D>,
84-
) -> Result<(), ProviderError> {
85-
let (_response_url, bytes) = Self::fetch_inner(client, request).await?;
86-
handler.bytes(doc_id, bytes, res_callback);
87-
Ok(())
88-
}
89-
9080
#[allow(clippy::type_complexity)]
9181
pub fn fetch_with_callback(
9282
&self,
@@ -108,24 +98,78 @@ impl<D: 'static> Provider<D> {
10898
}
10999

110100
impl<D: 'static> NetProvider<D> for Provider<D> {
111-
fn fetch(&self, doc_id: usize, request: Request, handler: BoxedHandler<D>) {
101+
fn fetch(&self, doc_id: usize, mut request: Request, handler: BoxedHandler<D>) {
112102
let client = self.client.clone();
113103
let callback = Arc::clone(&self.resource_callback);
114104
println!("Fetching {}", &request.url);
115105
self.rt.spawn(async move {
116106
let url = request.url.to_string();
117-
let res = Self::fetch_with_handler(client, doc_id, request, handler, callback).await;
118-
if let Err(e) = res {
119-
eprintln!("Error fetching {url}: {e:?}");
107+
let signal = request.signal.take();
108+
let result = if let Some(signal) = signal {
109+
AbortFetch::new(
110+
signal,
111+
Box::pin(async move { Self::fetch_inner(client, request).await }),
112+
)
113+
.await
120114
} else {
121-
println!("Success {url}");
115+
Self::fetch_inner(client, request).await
116+
};
117+
118+
match result {
119+
Ok((_response_url, bytes)) => {
120+
handler.bytes(doc_id, bytes, callback);
121+
println!("Success {url}");
122+
}
123+
Err(e) => {
124+
eprintln!("Error fetching {url}: {e:?}");
125+
}
122126
}
123127
});
124128
}
125129
}
126130

131+
struct AbortFetch<F, T> {
132+
signal: AbortSignal,
133+
future: F,
134+
_rt: PhantomData<T>,
135+
}
136+
137+
impl<F, T> AbortFetch<F, T> {
138+
fn new(signal: AbortSignal, future: F) -> Self {
139+
Self {
140+
signal,
141+
future,
142+
_rt: PhantomData,
143+
}
144+
}
145+
}
146+
147+
impl<F, T> Future for AbortFetch<F, T>
148+
where
149+
F: Future + Unpin + Send + 'static,
150+
F::Output: Send + Into<Result<T, ProviderError>> + 'static,
151+
T: Unpin,
152+
{
153+
type Output = Result<T, ProviderError>;
154+
155+
fn poll(
156+
mut self: std::pin::Pin<&mut Self>,
157+
cx: &mut std::task::Context<'_>,
158+
) -> std::task::Poll<Self::Output> {
159+
if self.signal.aborted() {
160+
return Poll::Ready(Err(ProviderError::Abort));
161+
}
162+
163+
match Pin::new(&mut self.future).poll(cx) {
164+
Poll::Ready(output) => Poll::Ready(output.into()),
165+
Poll::Pending => Poll::Pending,
166+
}
167+
}
168+
}
169+
127170
#[derive(Debug)]
128171
pub enum ProviderError {
172+
Abort,
129173
Io(std::io::Error),
130174
DataUrl(data_url::DataUrlError),
131175
DataUrlBase64(data_url::forgiving_base64::InvalidBase64),

packages/blitz-traits/src/navigation.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,15 @@ impl NavigationOptions {
5858
method: Method::POST,
5959
headers,
6060
body: document_resource,
61+
signal: None,
6162
}
6263
} else {
6364
Request {
6465
url: self.url,
6566
method: Method::GET,
6667
headers,
6768
body: Bytes::new(),
69+
signal: None,
6870
}
6971
}
7072
}

packages/blitz-traits/src/net.rs

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
pub use bytes::Bytes;
22
pub use http::{self, HeaderMap, Method};
3-
use std::sync::Arc;
3+
use std::sync::{
4+
Arc,
5+
atomic::{AtomicBool, Ordering},
6+
};
47
pub use url::Url;
58

69
pub type SharedProvider<D> = Arc<dyn NetProvider<D>>;
@@ -40,6 +43,7 @@ pub struct Request {
4043
pub method: Method,
4144
pub headers: HeaderMap,
4245
pub body: Bytes,
46+
pub signal: Option<AbortSignal>,
4347
}
4448
impl Request {
4549
/// A get request to the specified Url and an empty body
@@ -49,8 +53,14 @@ impl Request {
4953
method: Method::GET,
5054
headers: HeaderMap::new(),
5155
body: Bytes::new(),
56+
signal: None,
5257
}
5358
}
59+
60+
pub fn signal(mut self, signal: AbortSignal) -> Self {
61+
self.signal = Some(signal);
62+
self
63+
}
5464
}
5565

5666
/// A default noop NetProvider
@@ -66,3 +76,42 @@ pub struct DummyNetCallback;
6676
impl<D: Send + Sync + 'static> NetCallback<D> for DummyNetCallback {
6777
fn call(&self, _doc_id: usize, _result: Result<D, Option<String>>) {}
6878
}
79+
80+
/// The AbortController interface represents a controller object that
81+
/// allows you to abort one or more Web requests as and when desired.
82+
///
83+
/// https://developer.mozilla.org/en-US/docs/Web/API/AbortController
84+
#[derive(Debug, Default)]
85+
pub struct AbortController {
86+
pub signal: AbortSignal,
87+
}
88+
89+
impl AbortController {
90+
/// The abort() method of the AbortController interface aborts
91+
/// an asynchronous operation before it has completed.
92+
/// This is able to abort fetch requests.
93+
///
94+
/// https://developer.mozilla.org/en-US/docs/Web/API/AbortController/abort
95+
pub fn abort(self) {
96+
self.signal.0.store(true, Ordering::SeqCst);
97+
}
98+
}
99+
100+
/// The AbortSignal interface represents a signal object that allows you to
101+
/// communicate with an asynchronous operation (such as a fetch request) and
102+
/// abort it if required via an AbortController object.
103+
///
104+
/// https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal
105+
#[derive(Debug, Default, Clone)]
106+
pub struct AbortSignal(Arc<AtomicBool>);
107+
108+
impl AbortSignal {
109+
/// The aborted read-only property returns a value that indicates whether
110+
/// the asynchronous operations the signal is communicating with are
111+
/// aborted (true) or not (false).
112+
///
113+
/// https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/aborted
114+
pub fn aborted(&self) -> bool {
115+
self.0.load(Ordering::SeqCst)
116+
}
117+
}

0 commit comments

Comments
 (0)