diff --git a/Libraries/LibWeb/HTML/BroadcastChannel.cpp b/Libraries/LibWeb/HTML/BroadcastChannel.cpp
index b421898dd69..ccd7a9d5c2f 100644
--- a/Libraries/LibWeb/HTML/BroadcastChannel.cpp
+++ b/Libraries/LibWeb/HTML/BroadcastChannel.cpp
@@ -1,5 +1,6 @@
/*
* Copyright (c) 2024, Jamie Mansfield
+ * Copyright (c) 2024, Shannon Booth
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@@ -9,14 +10,62 @@
#include
#include
#include
+#include
+#include
+#include
+#include
namespace Web::HTML {
+class BroadcastChannelRepository {
+public:
+ void register_channel(GC::Root);
+ void unregister_channel(GC::Ref);
+ Vector> const& registered_channels_for_key(StorageAPI::StorageKey) const;
+
+private:
+ HashMap>> m_channels;
+};
+
+void BroadcastChannelRepository::register_channel(GC::Root 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> channels;
+ channels.append(move(channel));
+ m_channels.set(storage_key, move(channels));
+}
+
+void BroadcastChannelRepository::unregister_channel(GC::Ref 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> 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::construct_impl(JS::Realm& realm, FlyString const& name)
{
- return realm.create(realm, name);
+ auto channel = realm.create(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(global))
+ return static_cast(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(global))
+ return !static_cast(global).is_closing();
+
+ return false;
+}
+
+// https://html.spec.whatwg.org/multipage/web-messaging.html#dom-broadcastchannel-postmessage
+WebIDL::ExceptionOr 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> 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
diff --git a/Libraries/LibWeb/HTML/BroadcastChannel.h b/Libraries/LibWeb/HTML/BroadcastChannel.h
index 19ee7033bbc..87fb8280ef3 100644
--- a/Libraries/LibWeb/HTML/BroadcastChannel.h
+++ b/Libraries/LibWeb/HTML/BroadcastChannel.h
@@ -1,5 +1,6 @@
/*
* Copyright (c) 2024, Jamie Mansfield
+ * Copyright (c) 2024, Shannon Booth
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@@ -24,6 +25,8 @@ public:
return m_channel_name;
}
+ WebIDL::ExceptionOr post_message(JS::Value message);
+
void close();
void set_onmessage(GC::Ptr);
@@ -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 };
};
diff --git a/Libraries/LibWeb/HTML/BroadcastChannel.idl b/Libraries/LibWeb/HTML/BroadcastChannel.idl
index 6b415bb7d48..04fec6196cf 100644
--- a/Libraries/LibWeb/HTML/BroadcastChannel.idl
+++ b/Libraries/LibWeb/HTML/BroadcastChannel.idl
@@ -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;
diff --git a/Tests/LibWeb/Text/expected/wpt-import/webmessaging/broadcastchannel/basics.any.txt b/Tests/LibWeb/Text/expected/wpt-import/webmessaging/broadcastchannel/basics.any.txt
new file mode 100644
index 00000000000..3b65cd06f92
--- /dev/null
+++ b/Tests/LibWeb/Text/expected/wpt-import/webmessaging/broadcastchannel/basics.any.txt
@@ -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
\ No newline at end of file
diff --git a/Tests/LibWeb/Text/expected/wpt-import/webmessaging/broadcastchannel/interface.any.txt b/Tests/LibWeb/Text/expected/wpt-import/webmessaging/broadcastchannel/interface.any.txt
new file mode 100644
index 00000000000..3f0af5285b4
--- /dev/null
+++ b/Tests/LibWeb/Text/expected/wpt-import/webmessaging/broadcastchannel/interface.any.txt
@@ -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
\ No newline at end of file
diff --git a/Tests/LibWeb/Text/input/wpt-import/webmessaging/broadcastchannel/basics.any.html b/Tests/LibWeb/Text/input/wpt-import/webmessaging/broadcastchannel/basics.any.html
new file mode 100644
index 00000000000..b6d12e0ad8e
--- /dev/null
+++ b/Tests/LibWeb/Text/input/wpt-import/webmessaging/broadcastchannel/basics.any.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
diff --git a/Tests/LibWeb/Text/input/wpt-import/webmessaging/broadcastchannel/basics.any.js b/Tests/LibWeb/Text/input/wpt-import/webmessaging/broadcastchannel/basics.any.js
new file mode 100644
index 00000000000..eec09d65a3a
--- /dev/null
+++ b/Tests/LibWeb/Text/input/wpt-import/webmessaging/broadcastchannel/basics.any.js
@@ -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');
diff --git a/Tests/LibWeb/Text/input/wpt-import/webmessaging/broadcastchannel/interface.any.html b/Tests/LibWeb/Text/input/wpt-import/webmessaging/broadcastchannel/interface.any.html
new file mode 100644
index 00000000000..4df53761ea3
--- /dev/null
+++ b/Tests/LibWeb/Text/input/wpt-import/webmessaging/broadcastchannel/interface.any.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
diff --git a/Tests/LibWeb/Text/input/wpt-import/webmessaging/broadcastchannel/interface.any.js b/Tests/LibWeb/Text/input/wpt-import/webmessaging/broadcastchannel/interface.any.js
new file mode 100644
index 00000000000..35e09d34b41
--- /dev/null
+++ b/Tests/LibWeb/Text/input/wpt-import/webmessaging/broadcastchannel/interface.any.js
@@ -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');