mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-07-29 20:29:18 +00:00
LibWeb/FileAPI: Implement aborting a FileReader read
This fixes a timeout for the included WPT test.
This commit is contained in:
parent
33e80fbf51
commit
8e410f959c
Notes:
github-actions[bot]
2025-01-30 21:26:29 +00:00
Author: https://github.com/shannonbooth
Commit: 8e410f959c
Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/3144
Reviewed-by: https://github.com/ADKaster ✅
5 changed files with 153 additions and 8 deletions
|
@ -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 fr’s state to "loading".
|
||||
m_state = State::Loading;
|
||||
m_is_aborted = false;
|
||||
|
||||
// 3. Set fr’s 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 fr’s 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 fr’s 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));
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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
|
|
@ -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>
|
|
@ -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');
|
Loading…
Add table
Add a link
Reference in a new issue