LibWeb: Implement BroadcastChannel.postMessage
Some checks are pending
CI / Lagom (false, FUZZ, ubuntu-24.04, Linux, Clang) (push) Waiting to run
CI / Lagom (false, NO_FUZZ, macos-15, macOS, Clang) (push) Waiting to run
CI / Lagom (false, NO_FUZZ, ubuntu-24.04, Linux, GNU) (push) Waiting to run
CI / Lagom (true, NO_FUZZ, ubuntu-24.04, Linux, Clang) (push) Waiting to run
Package the js repl as a binary artifact / build-and-package (macos-14, macOS, macOS-universal2) (push) Waiting to run
Package the js repl as a binary artifact / build-and-package (ubuntu-24.04, Linux, Linux-x86_64) (push) Waiting to run
Run test262 and test-wasm / run_and_update_results (push) Waiting to run
Lint Code / lint (push) Waiting to run
Push notes / build (push) Waiting to run

The repository being in static storage is a bit of a hodgepodge, but in
line with how our current storage partitioning is done. We should
eventually move this, along with other across browsing context APIs to a
proper location at a later stage. But for now, this makes progress on
the meat of the BroadcastChannel API.
This commit is contained in:
Shannon Booth 2024-11-23 20:45:26 +13:00 committed by Andreas Kling
parent 8f6fe1de83
commit e565e3c557
Notes: github-actions[bot] 2024-11-23 15:44:45 +00:00
9 changed files with 422 additions and 2 deletions

View file

@ -1,5 +1,6 @@
/*
* Copyright (c) 2024, Jamie Mansfield <jmansfield@cadixdev.org>
* Copyright (c) 2024, Shannon Booth <shannon@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@ -9,14 +10,62 @@
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/HTML/BroadcastChannel.h>
#include <LibWeb/HTML/EventNames.h>
#include <LibWeb/HTML/MessageEvent.h>
#include <LibWeb/HTML/Window.h>
#include <LibWeb/HTML/WorkerGlobalScope.h>
#include <LibWeb/StorageAPI/StorageKey.h>
namespace Web::HTML {
class BroadcastChannelRepository {
public:
void register_channel(GC::Root<BroadcastChannel>);
void unregister_channel(GC::Ref<BroadcastChannel>);
Vector<GC::Root<BroadcastChannel>> const& registered_channels_for_key(StorageAPI::StorageKey) const;
private:
HashMap<StorageAPI::StorageKey, Vector<GC::Root<BroadcastChannel>>> m_channels;
};
void BroadcastChannelRepository::register_channel(GC::Root<BroadcastChannel> channel)
{
auto storage_key = Web::StorageAPI::obtain_a_storage_key_for_non_storage_purposes(relevant_settings_object(*channel));
auto maybe_channels = m_channels.find(storage_key);
if (maybe_channels != m_channels.end()) {
maybe_channels->value.append(move(channel));
return;
}
Vector<GC::Root<BroadcastChannel>> channels;
channels.append(move(channel));
m_channels.set(storage_key, move(channels));
}
void BroadcastChannelRepository::unregister_channel(GC::Ref<BroadcastChannel> channel)
{
auto storage_key = Web::StorageAPI::obtain_a_storage_key_for_non_storage_purposes(relevant_settings_object(channel));
auto& relevant_channels = m_channels.get(storage_key).value();
relevant_channels.remove_first_matching([&](auto c) { return c == channel; });
}
Vector<GC::Root<BroadcastChannel>> const& BroadcastChannelRepository::registered_channels_for_key(StorageAPI::StorageKey key) const
{
auto maybe_channels = m_channels.get(key);
VERIFY(maybe_channels.has_value());
return maybe_channels.value();
}
// FIXME: This should not be static, and live at a storage partitioned level of the user agent.
static BroadcastChannelRepository s_broadcast_channel_repository;
GC_DEFINE_ALLOCATOR(BroadcastChannel);
GC::Ref<BroadcastChannel> BroadcastChannel::construct_impl(JS::Realm& realm, FlyString const& name)
{
return realm.create<BroadcastChannel>(realm, name);
auto channel = realm.create<BroadcastChannel>(realm, name);
s_broadcast_channel_repository.register_channel(channel);
return channel;
}
BroadcastChannel::BroadcastChannel(JS::Realm& realm, FlyString const& name)
@ -31,11 +80,114 @@ void BroadcastChannel::initialize(JS::Realm& realm)
WEB_SET_PROTOTYPE_FOR_INTERFACE(BroadcastChannel);
}
// https://html.spec.whatwg.org/multipage/web-messaging.html#eligible-for-messaging
bool BroadcastChannel::is_eligible_for_messaging() const
{
// A BroadcastChannel object is said to be eligible for messaging when its relevant global object is either:
auto const& global = relevant_global_object(*this);
// * a Window object whose associated Document is fully active, or
if (is<Window>(global))
return static_cast<Window const&>(global).associated_document().is_fully_active();
// * a WorkerGlobalScope object whose closing flag is false and whose worker is not a suspendable worker.
// FIXME: Suspendable worker
if (is<WorkerGlobalScope>(global))
return !static_cast<WorkerGlobalScope const&>(global).is_closing();
return false;
}
// https://html.spec.whatwg.org/multipage/web-messaging.html#dom-broadcastchannel-postmessage
WebIDL::ExceptionOr<void> BroadcastChannel::post_message(JS::Value message)
{
auto& vm = this->vm();
// 1. If this is not eligible for messaging, then return.
if (!is_eligible_for_messaging())
return {};
// 2. If this's closed flag is true, then throw an "InvalidStateError" DOMException.
if (m_closed_flag)
return WebIDL::InvalidStateError::create(realm(), "BroadcastChannel.postMessage() on a closed channel"_string);
// 3. Let serialized be StructuredSerialize(message). Rethrow any exceptions.
auto serialized = TRY(structured_serialize(vm, message));
// 4. Let sourceOrigin be this's relevant settings object's origin.
auto source_origin = relevant_settings_object(*this).origin();
// 5. Let sourceStorageKey be the result of running obtain a storage key for non-storage purposes with this's relevant settings object.
auto source_storage_key = Web::StorageAPI::obtain_a_storage_key_for_non_storage_purposes(relevant_settings_object(*this));
// 6. Let destinations be a list of BroadcastChannel objects that match the following criteria:
GC::MarkedVector<GC::Ref<BroadcastChannel>> destinations(vm.heap());
// * The result of running obtain a storage key for non-storage purposes with their relevant settings object equals sourceStorageKey.
auto same_origin_broadcast_channels = s_broadcast_channel_repository.registered_channels_for_key(source_storage_key);
for (auto const& channel : same_origin_broadcast_channels) {
// * They are eligible for messaging.
if (!channel->is_eligible_for_messaging())
continue;
// * Their channel name is this's channel name.
if (channel->name() != name())
continue;
destinations.append(*channel);
}
// 7. Remove source from destinations.
destinations.remove_first_matching([&](auto destination) { return destination == this; });
// FIXME: 8. Sort destinations such that all BroadcastChannel objects whose relevant agents are the same are sorted in creation order, oldest first.
// (This does not define a complete ordering. Within this constraint, user agents may sort the list in any implementation-defined manner.)
// 9. For each destination in destinations, queue a global task on the DOM manipulation task source given destination's relevant global object to perform the following steps:
for (auto destination : destinations) {
HTML::queue_global_task(HTML::Task::Source::DOMManipulation, relevant_global_object(destination), GC::create_function(vm.heap(), [&vm, serialized, destination, source_origin] {
// 1. If destination's closed flag is true, then abort these steps.
if (destination->m_closed_flag)
return;
// 2. Let targetRealm be destination's relevant realm.
auto& target_realm = relevant_realm(destination);
// 3. Let data be StructuredDeserialize(serialized, targetRealm).
// If this throws an exception, catch it, fire an event named messageerror at destination, using MessageEvent, with the
// origin attribute initialized to the serialization of sourceOrigin, and then abort these steps.
auto data_or_error = structured_deserialize(vm, serialized, target_realm);
if (data_or_error.is_exception()) {
MessageEventInit event_init {};
event_init.origin = source_origin.serialize();
auto event = MessageEvent::create(target_realm, HTML::EventNames::messageerror, event_init);
event->set_is_trusted(true);
destination->dispatch_event(event);
return;
}
// 4. Fire an event named message at destination, using MessageEvent, with the data attribute initialized to data and the
// origin attribute initialized to the serialization of sourceOrigin.
MessageEventInit event_init {};
event_init.data = data_or_error.release_value();
event_init.origin = source_origin.serialize();
auto event = MessageEvent::create(target_realm, HTML::EventNames::message, event_init);
event->set_is_trusted(true);
destination->dispatch_event(event);
}));
}
return {};
}
// https://html.spec.whatwg.org/multipage/web-messaging.html#dom-broadcastchannel-close
void BroadcastChannel::close()
{
// The close() method steps are to set this's closed flag to true.
m_closed_flag = true;
s_broadcast_channel_repository.unregister_channel(*this);
}
// https://html.spec.whatwg.org/multipage/web-messaging.html#handler-broadcastchannel-onmessage

View file

@ -1,5 +1,6 @@
/*
* Copyright (c) 2024, Jamie Mansfield <jmansfield@cadixdev.org>
* Copyright (c) 2024, Shannon Booth <shannon@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@ -24,6 +25,8 @@ public:
return m_channel_name;
}
WebIDL::ExceptionOr<void> post_message(JS::Value message);
void close();
void set_onmessage(GC::Ptr<WebIDL::CallbackType>);
@ -36,6 +39,8 @@ private:
virtual void initialize(JS::Realm&) override;
bool is_eligible_for_messaging() const;
FlyString m_channel_name;
bool m_closed_flag { false };
};

View file

@ -7,7 +7,7 @@ interface BroadcastChannel : EventTarget {
constructor(DOMString name);
readonly attribute DOMString name;
[FIXME] undefined postMessage(any message);
undefined postMessage(any message);
undefined close();
attribute EventHandler onmessage;
attribute EventHandler onmessageerror;

View file

@ -0,0 +1,17 @@
Summary
Harness status: OK
Rerun
Found 7 tests
7 Pass
Details
Result Test Name MessagePass BroadcastChannel constructor called as normal function
Pass postMessage results in correct event
Pass messages are delivered in port creation order
Pass messages aren't delivered to a closed port
Pass messages aren't delivered to a port closed after calling postMessage.
Pass closing and creating channels during message delivery works correctly
Pass Closing a channel in onmessage prevents already queued tasks from firing onmessage events

View file

@ -0,0 +1,23 @@
Summary
Harness status: OK
Rerun
Found 13 tests
13 Pass
Details
Result Test Name MessagePass Should throw if no name is provided
Pass Null name should not throw
Pass Undefined name should not throw
Pass Non-empty name should not throw
Pass Non-string name should not throw
Pass postMessage without parameters should throw
Pass postMessage with null should not throw
Pass close should not throw
Pass close should not throw when called multiple times
Pass postMessage after close should throw
Pass BroadcastChannel should have an onmessage event
Pass postMessage should throw with uncloneable data
Pass postMessage should throw InvalidStateError after close, even with uncloneable data

View file

@ -0,0 +1,15 @@
<!doctype html>
<meta charset=utf-8>
<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="../../webmessaging/broadcastchannel/basics.any.js"></script>

View file

@ -0,0 +1,128 @@
test(function() {
assert_throws_js(
TypeError,
() => BroadcastChannel(""),
"Calling BroadcastChannel constructor without 'new' must throw"
);
}, "BroadcastChannel constructor called as normal function");
async_test(t => {
let c1 = new BroadcastChannel('eventType');
let c2 = new BroadcastChannel('eventType');
c2.onmessage = t.step_func(e => {
assert_true(e instanceof MessageEvent);
assert_equals(e.target, c2);
assert_equals(e.type, 'message');
assert_equals(e.origin, location.origin, 'origin');
assert_equals(e.data, 'hello world');
assert_equals(e.source, null, 'source');
t.done();
});
c1.postMessage('hello world');
}, 'postMessage results in correct event');
async_test(t => {
let c1 = new BroadcastChannel('order');
let c2 = new BroadcastChannel('order');
let c3 = new BroadcastChannel('order');
let events = [];
let doneCount = 0;
let handler = t.step_func(e => {
events.push(e);
if (e.data == 'done') {
doneCount++;
if (doneCount == 2) {
assert_equals(events.length, 6);
assert_equals(events[0].target, c2, 'target for event 0');
assert_equals(events[0].data, 'from c1');
assert_equals(events[1].target, c3, 'target for event 1');
assert_equals(events[1].data, 'from c1');
assert_equals(events[2].target, c1, 'target for event 2');
assert_equals(events[2].data, 'from c3');
assert_equals(events[3].target, c2, 'target for event 3');
assert_equals(events[3].data, 'from c3');
assert_equals(events[4].target, c1, 'target for event 4');
assert_equals(events[4].data, 'done');
assert_equals(events[5].target, c3, 'target for event 5');
assert_equals(events[5].data, 'done');
t.done();
}
}
});
c1.onmessage = handler;
c2.onmessage = handler;
c3.onmessage = handler;
c1.postMessage('from c1');
c3.postMessage('from c3');
c2.postMessage('done');
}, 'messages are delivered in port creation order');
async_test(t => {
let c1 = new BroadcastChannel('closed');
let c2 = new BroadcastChannel('closed');
let c3 = new BroadcastChannel('closed');
c2.onmessage = t.unreached_func();
c2.close();
c3.onmessage = t.step_func(() => t.done());
c1.postMessage('test');
}, 'messages aren\'t delivered to a closed port');
async_test(t => {
let c1 = new BroadcastChannel('closed');
let c2 = new BroadcastChannel('closed');
let c3 = new BroadcastChannel('closed');
c2.onmessage = t.unreached_func();
c3.onmessage = t.step_func(() => t.done());
c1.postMessage('test');
c2.close();
}, 'messages aren\'t delivered to a port closed after calling postMessage.');
async_test(t => {
let c1 = new BroadcastChannel('create-in-onmessage');
let c2 = new BroadcastChannel('create-in-onmessage');
c2.onmessage = t.step_func(e => {
assert_equals(e.data, 'first');
c2.close();
let c3 = new BroadcastChannel('create-in-onmessage');
c3.onmessage = t.step_func(event => {
assert_equals(event.data, 'done');
t.done();
});
c1.postMessage('done');
});
c1.postMessage('first');
c2.postMessage('second');
}, 'closing and creating channels during message delivery works correctly');
async_test(t => {
let c1 = new BroadcastChannel('close-in-onmessage');
let c2 = new BroadcastChannel('close-in-onmessage');
let c3 = new BroadcastChannel('close-in-onmessage');
let events = [];
c1.onmessage = e => events.push('c1: ' + e.data);
c2.onmessage = e => events.push('c2: ' + e.data);
c3.onmessage = e => events.push('c3: ' + e.data);
// c2 closes itself when it receives the first message
c2.addEventListener('message', e => {
c2.close();
});
c3.addEventListener('message', t.step_func(e => {
if (e.data == 'done') {
assert_array_equals(events, [
'c2: first',
'c3: first',
'c3: done']);
t.done();
}
}));
c1.postMessage('first');
c1.postMessage('done');
}, 'Closing a channel in onmessage prevents already queued tasks from firing onmessage events');

View file

@ -0,0 +1,15 @@
<!doctype html>
<meta charset=utf-8>
<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="../../webmessaging/broadcastchannel/interface.any.js"></script>

View file

@ -0,0 +1,65 @@
test(() => assert_throws_js(TypeError, () => new BroadcastChannel()),
'Should throw if no name is provided');
test(() => {
let c = new BroadcastChannel(null);
assert_equals(c.name, 'null');
}, 'Null name should not throw');
test(() => {
let c = new BroadcastChannel(undefined);
assert_equals(c.name, 'undefined');
}, 'Undefined name should not throw');
test(() => {
let c = new BroadcastChannel('fooBar');
assert_equals(c.name, 'fooBar');
}, 'Non-empty name should not throw');
test(() => {
let c = new BroadcastChannel(123);
assert_equals(c.name, '123');
}, 'Non-string name should not throw');
test(() => {
let c = new BroadcastChannel('');
assert_throws_js(TypeError, () => c.postMessage());
}, 'postMessage without parameters should throw');
test(() => {
let c = new BroadcastChannel('');
c.postMessage(null);
}, 'postMessage with null should not throw');
test(() => {
let c = new BroadcastChannel('');
c.close();
}, 'close should not throw');
test(() => {
let c = new BroadcastChannel('');
c.close();
c.close();
}, 'close should not throw when called multiple times');
test(() => {
let c = new BroadcastChannel('');
c.close();
assert_throws_dom('InvalidStateError', () => c.postMessage(''));
}, 'postMessage after close should throw');
test(() => {
let c = new BroadcastChannel('');
assert_not_equals(c.onmessage, undefined);
}, 'BroadcastChannel should have an onmessage event');
test(() => {
let c = new BroadcastChannel('');
assert_throws_dom('DataCloneError', () => c.postMessage(Symbol()));
}, 'postMessage should throw with uncloneable data');
test(() => {
let c = new BroadcastChannel('');
c.close();
assert_throws_dom('InvalidStateError', () => c.postMessage(Symbol()));
}, 'postMessage should throw InvalidStateError after close, even with uncloneable data');