LibWeb/FileAPI: Implement aborting a FileReader read

This fixes a timeout for the included WPT test.
This commit is contained in:
Shannon Booth 2025-01-05 11:05:53 +13:00 committed by Andrew Kaster
commit 8e410f959c
Notes: github-actions[bot] 2025-01-30 21:26:29 +00:00
5 changed files with 153 additions and 8 deletions

View file

@ -21,6 +21,7 @@
#include <LibWeb/FileAPI/FileReader.h>
#include <LibWeb/HTML/EventLoop/EventLoop.h>
#include <LibWeb/HTML/EventNames.h>
#include <LibWeb/HTML/Scripting/Agent.h>
#include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h>
#include <LibWeb/MimeSniff/MimeType.h>
#include <LibWeb/Platform/EventLoopPlugin.h>
@ -115,6 +116,26 @@ WebIDL::ExceptionOr<FileReader::Result> FileReader::blob_package_data(JS::Realm&
VERIFY_NOT_REACHED();
}
void FileReader::queue_a_task(GC::Ref<GC::Function<void()>> task)
{
// To implement the requirement of removing queued tasks on an abort we keep track of a list of
// task IDs which are pending evaluation. This allows an abort to go through the task queue to
// remove those pending tasks.
auto wrapper_task = GC::create_function(heap(), [this, task] {
auto& event_loop = *HTML::relevant_agent(*this).event_loop;
VERIFY(event_loop.currently_running_task());
auto& current_task = *event_loop.currently_running_task();
task->function()();
m_pending_tasks.remove(current_task.id());
});
auto id = HTML::queue_global_task(HTML::Task::Source::FileReading, realm().global_object(), wrapper_task);
m_pending_tasks.set(id);
}
// https://w3c.github.io/FileAPI/#readOperation
WebIDL::ExceptionOr<void> FileReader::read_operation(Blob& blob, Type type, Optional<String> const& encoding_name)
{
@ -127,6 +148,7 @@ WebIDL::ExceptionOr<void> FileReader::read_operation(Blob& blob, Type type, Opti
// 2. Set frs state to "loading".
m_state = State::Loading;
m_is_aborted = false;
// 3. Set frs result to null.
m_result = {};
@ -154,7 +176,7 @@ WebIDL::ExceptionOr<void> FileReader::read_operation(Blob& blob, Type type, Opti
HTML::TemporaryExecutionContext execution_context { realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes };
Optional<MonotonicTime> progress_timer;
while (true) {
while (!m_is_aborted) {
auto& vm = realm.vm();
// FIXME: Try harder to not reach into the [[Promise]] slot of chunkPromise
auto promise = GC::Ref { as<JS::Promise>(*chunk_promise->promise()) };
@ -165,10 +187,13 @@ WebIDL::ExceptionOr<void> FileReader::read_operation(Blob& blob, Type type, Opti
return promise->state() == JS::Promise::State::Fulfilled || promise->state() == JS::Promise::State::Rejected;
}));
if (m_is_aborted)
return;
// 2. If chunkPromise is fulfilled, and isFirstChunk is true, queue a task to fire a progress event called loadstart at fr.
// NOTE: ISSUE 2 We might change loadstart to be dispatched synchronously, to align with XMLHttpRequest behavior. [Issue #119]
if (promise->state() == JS::Promise::State::Fulfilled && is_first_chunk) {
HTML::queue_global_task(HTML::Task::Source::FileReading, realm.global_object(), GC::create_function(heap(), [this, &realm]() {
queue_a_task(GC::create_function(heap(), [this, &realm]() {
dispatch_event(DOM::Event::create(realm, HTML::EventNames::loadstart));
}));
}
@ -197,7 +222,7 @@ WebIDL::ExceptionOr<void> FileReader::read_operation(Blob& blob, Type type, Opti
// See http://wpt.live/FileAPI/reading-data-section/filereader_events.any.html
bool contained_data = byte_sequence.array_length().length() > 0;
if (enough_time_passed && contained_data) {
HTML::queue_global_task(HTML::Task::Source::FileReading, realm.global_object(), GC::create_function(heap(), [this, &realm]() {
queue_a_task(GC::create_function(heap(), [this, &realm]() {
dispatch_event(DOM::Event::create(realm, HTML::EventNames::progress));
}));
progress_timer = now;
@ -208,7 +233,7 @@ WebIDL::ExceptionOr<void> FileReader::read_operation(Blob& blob, Type type, Opti
}
// 5. Otherwise, if chunkPromise is fulfilled with an object whose done property is true, queue a task to run the following steps and abort this algorithm:
else if (promise->state() == JS::Promise::State::Fulfilled && done.as_bool()) {
HTML::queue_global_task(HTML::Task::Source::FileReading, realm.global_object(), GC::create_function(heap(), [this, bytes, type, &realm, encoding_name, blobs_type]() {
queue_a_task(GC::create_function(heap(), [this, bytes, type, &realm, encoding_name, blobs_type]() {
// 1. Set frs state to "done".
m_state = State::Done;
@ -242,7 +267,7 @@ WebIDL::ExceptionOr<void> FileReader::read_operation(Blob& blob, Type type, Opti
}
// 6. Otherwise, if chunkPromise is rejected with an error error, queue a task to run the following steps and abort this algorithm:
else if (promise->state() == JS::Promise::State::Rejected) {
HTML::queue_global_task(HTML::Task::Source::FileReading, realm.global_object(), GC::create_function(heap(), [this, &realm]() {
queue_a_task(GC::create_function(heap(), [this, &realm]() {
// 1. Set frs state to "done".
m_state = State::Done;
@ -312,9 +337,15 @@ void FileReader::abort()
m_result = {};
}
// FIXME: 3. If there are any tasks from this on the file reading task source in an affiliated task queue, then remove those tasks from that task queue.
// 3. If there are any tasks from this on the file reading task source in an affiliated task queue, then remove those tasks from that task queue.
auto& event_loop = *HTML::relevant_agent(*this).event_loop;
event_loop.task_queue().remove_tasks_matching([&](auto const& task) {
return m_pending_tasks.contains(task.id());
});
m_pending_tasks.clear();
// FIXME: 4. Terminate the algorithm for the read method being processed.
// 4. Terminate the algorithm for the read method being processed.
m_is_aborted = true;
// 5. Fire a progress event called abort at this.
dispatch_event(DOM::Event::create(realm, HTML::EventNames::abort));

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2023, Shannon Booth <shannon@serenityos.org>
* Copyright (c) 2023-2025, Shannon Booth <shannon@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@ -10,6 +10,7 @@
#include <LibWeb/Bindings/PlatformObject.h>
#include <LibWeb/DOM/EventTarget.h>
#include <LibWeb/Forward.h>
#include <LibWeb/HTML/EventLoop/Task.h>
#include <LibWeb/WebIDL/ExceptionOr.h>
namespace Web::FileAPI {
@ -101,6 +102,12 @@ private:
static WebIDL::ExceptionOr<Result> blob_package_data(JS::Realm& realm, ByteBuffer, FileReader::Type type, Optional<String> const&, Optional<String> const& encoding_name);
void queue_a_task(GC::Ref<GC::Function<void()>>);
// Internal state to handle aborting the FileReader.
HashTable<HTML::TaskID> m_pending_tasks;
bool m_is_aborted { false };
// A FileReader has an associated state, that is "empty", "loading", or "done". It is initially "empty".
// https://w3c.github.io/FileAPI/#filereader-state
State m_state { State::Empty };

View file

@ -0,0 +1,11 @@
Harness status: OK
Found 6 tests
6 Pass
Pass test FileReader InvalidStateError exception for readAsText
Pass test FileReader InvalidStateError exception for readAsDataURL
Pass test FileReader InvalidStateError exception for readAsArrayBuffer
Pass test FileReader InvalidStateError exception in onloadstart event for readAsArrayBuffer
Pass test FileReader no InvalidStateError exception in loadend event handler for readAsArrayBuffer
Pass test abort and restart in onloadstart event for readAsText

View file

@ -0,0 +1,15 @@
<!doctype html>
<meta charset=utf-8>
<title>FileReader: starting new reads while one is in progress</title>
<script>
self.GLOBAL = {
isWindow: function() { return true; },
isWorker: function() { return false; },
isShadowRealm: function() { return false; },
};
</script>
<script src="../../resources/testharness.js"></script>
<script src="../../resources/testharnessreport.js"></script>
<div id=log></div>
<script src="../../FileAPI/reading-data-section/FileReader-multiple-reads.any.js"></script>

View file

@ -0,0 +1,81 @@
// META: title=FileReader: starting new reads while one is in progress
test(function() {
var blob_1 = new Blob(['TEST000000001'])
var blob_2 = new Blob(['TEST000000002'])
var reader = new FileReader();
reader.readAsText(blob_1)
assert_equals(reader.readyState, FileReader.LOADING, "readyState Must be LOADING")
assert_throws_dom("InvalidStateError", function () {
reader.readAsText(blob_2)
})
}, 'test FileReader InvalidStateError exception for readAsText');
test(function() {
var blob_1 = new Blob(['TEST000000001'])
var blob_2 = new Blob(['TEST000000002'])
var reader = new FileReader();
reader.readAsDataURL(blob_1)
assert_equals(reader.readyState, FileReader.LOADING, "readyState Must be LOADING")
assert_throws_dom("InvalidStateError", function () {
reader.readAsDataURL(blob_2)
})
}, 'test FileReader InvalidStateError exception for readAsDataURL');
test(function() {
var blob_1 = new Blob(['TEST000000001'])
var blob_2 = new Blob(['TEST000000002'])
var reader = new FileReader();
reader.readAsArrayBuffer(blob_1)
assert_equals(reader.readyState, FileReader.LOADING, "readyState Must be LOADING")
assert_throws_dom("InvalidStateError", function () {
reader.readAsArrayBuffer(blob_2)
})
}, 'test FileReader InvalidStateError exception for readAsArrayBuffer');
async_test(function() {
var blob_1 = new Blob(['TEST000000001'])
var blob_2 = new Blob(['TEST000000002'])
var reader = new FileReader();
var triggered = false;
reader.onloadstart = this.step_func_done(function() {
assert_false(triggered, "Only one loadstart event should be dispatched");
triggered = true;
assert_equals(reader.readyState, FileReader.LOADING,
"readyState must be LOADING")
assert_throws_dom("InvalidStateError", function () {
reader.readAsArrayBuffer(blob_2)
})
});
reader.readAsArrayBuffer(blob_1)
assert_equals(reader.readyState, FileReader.LOADING, "readyState Must be LOADING")
}, 'test FileReader InvalidStateError exception in onloadstart event for readAsArrayBuffer');
async_test(function() {
var blob_1 = new Blob(['TEST000000001'])
var blob_2 = new Blob(['TEST000000002'])
var reader = new FileReader();
reader.onloadend = this.step_func_done(function() {
assert_equals(reader.readyState, FileReader.DONE,
"readyState must be DONE")
reader.readAsArrayBuffer(blob_2)
assert_equals(reader.readyState, FileReader.LOADING, "readyState Must be LOADING")
});
reader.readAsArrayBuffer(blob_1)
assert_equals(reader.readyState, FileReader.LOADING, "readyState Must be LOADING")
}, 'test FileReader no InvalidStateError exception in loadend event handler for readAsArrayBuffer');
async_test(function() {
var blob_1 = new Blob([new Uint8Array(0x414141)]);
var blob_2 = new Blob(['TEST000000002']);
var reader = new FileReader();
reader.onloadstart = this.step_func(function() {
reader.abort();
reader.onloadstart = null;
reader.onloadend = this.step_func_done(function() {
assert_equals('TEST000000002', reader.result);
});
reader.readAsText(blob_2);
});
reader.readAsText(blob_1);
}, 'test abort and restart in onloadstart event for readAsText');