Skip to content

Commit

Permalink
LibWeb/FileAPI: Implement aborting a FileReader read
Browse files Browse the repository at this point in the history
This fixes a timeout for the included WPT test.
  • Loading branch information
shannonbooth committed Jan 11, 2025
1 parent b1ef9e5 commit fbb9b28
Show file tree
Hide file tree
Showing 5 changed files with 153 additions and 8 deletions.
45 changes: 38 additions & 7 deletions Libraries/LibWeb/FileAPI/FileReader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -111,6 +112,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)
{
Expand All @@ -123,6 +144,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 = {};
Expand Down Expand Up @@ -150,7 +172,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 { verify_cast<JS::Promise>(*chunk_promise->promise()) };
Expand All @@ -161,10 +183,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));
}));
}
Expand Down Expand Up @@ -193,7 +218,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;
Expand All @@ -204,7 +229,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;

Expand Down Expand Up @@ -238,7 +263,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;

Expand Down Expand Up @@ -308,9 +333,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));
Expand Down
9 changes: 8 additions & 1 deletion Libraries/LibWeb/FileAPI/FileReader.h
Original file line number Diff line number Diff line change
@@ -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
*/
Expand All @@ -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 {
Expand Down Expand Up @@ -101,6 +102,12 @@ class FileReader : public DOM::EventTarget {

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 };
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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>
Original file line number Diff line number Diff line change
@@ -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');

0 comments on commit fbb9b28

Please sign in to comment.