Skip to content

Commit 3058274

Browse files
LibWeb: Use unbuffered network requests for all Fetch requests
Previously, unbuffered requests were only available as a special mode for EventSource. With this change, they are enabled by default, which means chunks can be read from the stream as soon as they arrive. This unlocks some interesting possibilities, such as starting to parse HTML documents before the entire response has been received (that, in turn, allows us to initiate subresource fetches earlier or begin executing scripts sooner), or start rendering videos before they are fully downloaded. Co-authored-by: Timothy Flynn <trflynn89@pm.me>
1 parent f942fef commit 3058274

File tree

4 files changed

+172
-171
lines changed

4 files changed

+172
-171
lines changed

Libraries/LibWeb/Fetch/Fetching/FetchedDataReceiver.cpp

Lines changed: 76 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/*
22
* Copyright (c) 2024, Tim Flynn <trflynn89@serenityos.org>
3+
* Copyright (c) 2025, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
34
*
45
* SPDX-License-Identifier: BSD-2-Clause
56
*/
@@ -36,35 +37,82 @@ void FetchedDataReceiver::visit_edges(Visitor& visitor)
3637

3738
void FetchedDataReceiver::set_pending_promise(GC::Ref<WebIDL::Promise> promise)
3839
{
39-
auto had_pending_promise = m_pending_promise != nullptr;
40+
VERIFY(!m_pending_promise);
41+
VERIFY(!m_has_unfulfilled_promise);
4042
m_pending_promise = promise;
4143

42-
if (!had_pending_promise && !m_buffer.is_empty()) {
43-
on_data_received(m_buffer);
44-
m_buffer.clear();
44+
if (!m_buffer.is_empty()) {
45+
pull_bytes_into_stream(move(m_buffer));
46+
} else if (m_lifecycle_state == LifecycleState::ReadyToClose) {
47+
close_stream();
4548
}
4649
}
4750

4851
// This implements the parallel steps of the pullAlgorithm in HTTP-network-fetch.
49-
// https://fetch.spec.whatwg.org/#ref-for-in-parallel
50-
void FetchedDataReceiver::on_data_received(ReadonlyBytes bytes)
52+
// https://fetch.spec.whatwg.org/#ref-for-in-parallel
53+
void FetchedDataReceiver::handle_network_bytes(ReadonlyBytes bytes, NetworkState state)
5154
{
52-
// FIXME: 1. If the size of buffer is smaller than a lower limit chosen by the user agent and the ongoing fetch
53-
// is suspended, resume the fetch.
54-
// FIXME: 2. Wait until buffer is not empty.
55+
VERIFY(m_lifecycle_state == LifecycleState::Receiving);
56+
57+
if (state == NetworkState::Complete) {
58+
VERIFY(bytes.is_empty());
59+
m_lifecycle_state = LifecycleState::CompletePending;
60+
}
5561

56-
// If the remote end sends data immediately after we receive headers, we will often get that data here before the
57-
// stream tasks have all been queued internally. Just hold onto that data.
5862
if (!m_pending_promise) {
59-
m_buffer.append(bytes);
63+
if (state == NetworkState::Ongoing)
64+
m_buffer.append(bytes);
65+
if (m_lifecycle_state == LifecycleState::CompletePending && m_buffer.is_empty() && !m_has_unfulfilled_promise)
66+
m_lifecycle_state = LifecycleState::ReadyToClose;
6067
return;
6168
}
6269

70+
// 1. If one or more bytes have been transmitted from response’s message body, then:
71+
if (!bytes.is_empty()) {
72+
// 1. Let bytes be the transmitted bytes.
73+
74+
// FIXME: 2. Let codings be the result of extracting header list values given `Content-Encoding` and response’s header list.
75+
// FIXME: 3. Increase response’s body info’s encoded size by bytes’s length.
76+
// FIXME: 4. Set bytes to the result of handling content codings given codings and bytes.
77+
// FIXME: 5. Increase response’s body info’s decoded size by bytes’s length.
78+
// FIXME: 6. If bytes is failure, then terminate fetchParams’s controller.
79+
80+
// 7. Append bytes to buffer.
81+
pull_bytes_into_stream(MUST(ByteBuffer::copy(bytes)));
82+
83+
// FIXME: 8. If the size of buffer is larger than an upper limit chosen by the user agent, ask the user agent
84+
// to suspend the ongoing fetch.
85+
return;
86+
}
87+
// 2. Otherwise, if the bytes transmission for response’s message body is done normally and stream is readable,
88+
// then close stream, and abort these in-parallel steps.
89+
if (m_stream->is_readable()) {
90+
VERIFY(m_lifecycle_state == LifecycleState::CompletePending);
91+
close_stream();
92+
}
93+
}
94+
95+
// This implements the parallel steps of the pullAlgorithm in HTTP-network-fetch.
96+
// https://fetch.spec.whatwg.org/#ref-for-in-parallel④
97+
void FetchedDataReceiver::pull_bytes_into_stream(ByteBuffer&& bytes)
98+
{
99+
// FIXME: 1. If the size of buffer is smaller than a lower limit chosen by the user agent and the ongoing fetch
100+
// is suspended, resume the fetch.
101+
102+
// 2. Wait until buffer is not empty.
103+
VERIFY(!bytes.is_empty());
104+
VERIFY(m_lifecycle_state == LifecycleState::Receiving || m_lifecycle_state == LifecycleState::CompletePending);
105+
63106
// 3. Queue a fetch task to run the following steps, with fetchParams’s task destination.
107+
VERIFY(!m_has_unfulfilled_promise);
108+
m_has_unfulfilled_promise = true;
64109
Infrastructure::queue_fetch_task(
65110
m_fetch_params->controller(),
66111
m_fetch_params->task_destination(),
67-
GC::create_function(heap(), [this, bytes = MUST(ByteBuffer::copy(bytes))]() mutable {
112+
GC::create_function(heap(), [this, bytes = move(bytes), pending_promise = m_pending_promise]() mutable {
113+
m_has_unfulfilled_promise = false;
114+
VERIFY(m_lifecycle_state == LifecycleState::Receiving || m_lifecycle_state == LifecycleState::CompletePending);
115+
68116
HTML::TemporaryExecutionContext execution_context { m_stream->realm(), HTML::TemporaryExecutionContext::CallbacksEnabled::Yes };
69117

70118
// 1. Pull from bytes buffer into stream.
@@ -82,8 +130,22 @@ void FetchedDataReceiver::on_data_received(ReadonlyBytes bytes)
82130
m_fetch_params->controller()->terminate();
83131

84132
// 3. Resolve promise with undefined.
85-
WebIDL::resolve_promise(m_stream->realm(), *m_pending_promise, JS::js_undefined());
133+
WebIDL::resolve_promise(m_stream->realm(), *pending_promise, JS::js_undefined());
134+
135+
if (m_lifecycle_state == LifecycleState::CompletePending && m_buffer.is_empty())
136+
m_lifecycle_state = LifecycleState::ReadyToClose;
86137
}));
138+
139+
m_pending_promise = {};
140+
}
141+
142+
void FetchedDataReceiver::close_stream()
143+
{
144+
VERIFY(m_has_unfulfilled_promise == 0);
145+
WebIDL::resolve_promise(m_stream->realm(), *m_pending_promise, JS::js_undefined());
146+
m_pending_promise = {};
147+
m_lifecycle_state = LifecycleState::Closed;
148+
m_stream->close();
87149
}
88150

89151
}

Libraries/LibWeb/Fetch/Fetching/FetchedDataReceiver.h

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/*
22
* Copyright (c) 2024, Tim Flynn <trflynn89@serenityos.org>
3+
* Copyright (c) 2025, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
34
*
45
* SPDX-License-Identifier: BSD-2-Clause
56
*/
@@ -21,17 +22,35 @@ class FetchedDataReceiver final : public JS::Cell {
2122
virtual ~FetchedDataReceiver() override;
2223

2324
void set_pending_promise(GC::Ref<WebIDL::Promise>);
24-
void on_data_received(ReadonlyBytes);
25+
26+
enum class NetworkState {
27+
Ongoing,
28+
Complete,
29+
Error,
30+
};
31+
void handle_network_bytes(ReadonlyBytes, NetworkState);
2532

2633
private:
2734
FetchedDataReceiver(GC::Ref<Infrastructure::FetchParams const>, GC::Ref<Streams::ReadableStream>);
2835

2936
virtual void visit_edges(Visitor& visitor) override;
3037

38+
void pull_bytes_into_stream(ByteBuffer&&);
39+
void close_stream();
40+
3141
GC::Ref<Infrastructure::FetchParams const> m_fetch_params;
3242
GC::Ref<Streams::ReadableStream> m_stream;
3343
GC::Ptr<WebIDL::Promise> m_pending_promise;
44+
3445
ByteBuffer m_buffer;
46+
enum class LifecycleState {
47+
Receiving,
48+
CompletePending,
49+
ReadyToClose,
50+
Closed,
51+
};
52+
LifecycleState m_lifecycle_state { LifecycleState::Receiving };
53+
bool m_has_unfulfilled_promise { false };
3554
};
3655

3756
}

0 commit comments

Comments
 (0)