From e66571f3898cfc0ff3a4a97895692f37cd7eec98 Mon Sep 17 00:00:00 2001 From: Yagiz Nizipli Date: Mon, 13 Jan 2025 11:58:44 -0500 Subject: [PATCH] implement MessagePort and MessageChannel --- src/workerd/api/BUILD.bazel | 14 ++++ src/workerd/api/events.h | 67 +++++++++++++++- src/workerd/api/global-scope.h | 1 + src/workerd/api/message-channel.c++ | 66 ++++++++++++++++ src/workerd/api/message-channel.h | 116 ++++++++++++++++++++++++++++ src/workerd/api/web-socket.h | 61 +-------------- src/workerd/server/BUILD.bazel | 1 + src/workerd/server/workerd-api.c++ | 2 + 8 files changed, 267 insertions(+), 61 deletions(-) create mode 100644 src/workerd/api/message-channel.c++ create mode 100644 src/workerd/api/message-channel.h diff --git a/src/workerd/api/BUILD.bazel b/src/workerd/api/BUILD.bazel index 61625d9121a..47ce63f636e 100644 --- a/src/workerd/api/BUILD.bazel +++ b/src/workerd/api/BUILD.bazel @@ -23,6 +23,7 @@ filegroup( "rtti.c++", "url.c++", "util.c++", + "message-channel.c++", ], ), visibility = ["//visibility:public"], @@ -47,6 +48,7 @@ filegroup( "rtti.h", "url.h", "util.h", + "message-channel.h", ], ), visibility = ["//visibility:public"], @@ -198,6 +200,18 @@ wd_cc_library( ], ) +wd_cc_library( + name = "message-channel", + srcs = ["message-channel.c++"], + hdrs = ["message-channel.h"], + visibility = ["//visibility:public"], + deps = [ + "//src/workerd/io", + "//src/workerd/jsg:memory-tracker", + "@capnp-cpp//src/kj", + ], +) + wd_cc_library( name = "deferred-proxy", hdrs = ["deferred-proxy.h"], diff --git a/src/workerd/api/events.h b/src/workerd/api/events.h index be1f08771ae..32462f4324a 100644 --- a/src/workerd/api/events.h +++ b/src/workerd/api/events.h @@ -6,6 +6,70 @@ namespace workerd::api { +class CloseEvent: public Event { + public: + CloseEvent(): Event("close") {} + + CloseEvent(uint code, kj::String reason, bool clean) + : Event("close"), + code(code), + reason(kj::mv(reason)), + clean(clean) {} + CloseEvent(kj::String type, int code, kj::String reason, bool clean) + : Event(kj::mv(type)), + code(code), + reason(kj::mv(reason)), + clean(clean) {} + + struct Initializer { + jsg::Optional code; + jsg::Optional reason; + jsg::Optional wasClean; + + JSG_STRUCT(code, reason, wasClean); + JSG_STRUCT_TS_OVERRIDE(CloseEventInit); + }; + static jsg::Ref constructor(jsg::Optional type = kj::none, + jsg::Optional initializer = kj::none) { + KJ_IF_SOME(t, type) { + Initializer init = kj::mv(initializer).orDefault({}); + return jsg::alloc(kj::mv(t), init.code.orDefault(0), + kj::mv(init.reason).orDefault(nullptr), init.wasClean.orDefault(false)); + } + + return jsg::alloc(); + } + + int getCode() { + return code; + } + kj::Maybe getReason() { + return reason; + } + bool getWasClean() { + return clean; + } + + JSG_RESOURCE_TYPE(CloseEvent) { + JSG_INHERIT(Event); + + JSG_READONLY_INSTANCE_PROPERTY(code, getCode); + JSG_READONLY_INSTANCE_PROPERTY(reason, getReason); + JSG_READONLY_INSTANCE_PROPERTY(wasClean, getWasClean); + + JSG_TS_ROOT(); + } + + void visitForMemoryInfo(jsg::MemoryTracker& tracker) const { + tracker.trackField("reason", reason); + } + + private: + kj::uint code{}; + kj::Maybe reason; + bool clean{}; +}; + class ErrorEvent: public Event { public: struct ErrorEventInit { @@ -48,6 +112,7 @@ class ErrorEvent: public Event { void visitForGc(jsg::GcVisitor& visitor); }; -#define EW_EVENTS_ISOLATE_TYPES api::ErrorEvent, api::ErrorEvent::ErrorEventInit +#define EW_EVENTS_ISOLATE_TYPES \ + api::ErrorEvent, api::ErrorEvent::ErrorEventInit, api::CloseEvent, api::CloseEvent::Initializer } // namespace workerd::api diff --git a/src/workerd/api/global-scope.h b/src/workerd/api/global-scope.h index a8ed3f5f565..5ed0b382978 100644 --- a/src/workerd/api/global-scope.h +++ b/src/workerd/api/global-scope.h @@ -23,6 +23,7 @@ namespace workerd::api { class TailEvent; class Cache; class CacheStorage; +class CloseEvent; class Crypto; class CryptoKey; class ErrorEvent; diff --git a/src/workerd/api/message-channel.c++ b/src/workerd/api/message-channel.c++ new file mode 100644 index 00000000000..e7ad67dfc73 --- /dev/null +++ b/src/workerd/api/message-channel.c++ @@ -0,0 +1,66 @@ +#include "message-channel.h" + +#include +#include + +namespace workerd::api { + +MessagePort::~MessagePort() noexcept(false) { + // Technically we should dispatch a close event whenever this object + // is garbage collected. Unfortunately, we cannot guarantee that the destructor + // will always have the isolate lock. + // Let's keep in mind that the destructor might be running during v8 GC + // in which you shouldn't execute any javascript at all. + // dispatchEvent(js, Event::constructor(kj::str("close"), kj::none)); +} + +jsg::Ref MessagePort::constructor() { + return jsg::alloc(); +} + +void MessagePort::disentangle(jsg::Lock &js) { + KJ_IF_SOME(e, entangledWith) { + // Fire an event named close at otherPort. + e->dispatchEvent(js, CloseEvent::constructor()); + e->entangledWith = kj::none; + } + entangledWith = kj::none; +} + +void MessagePort::entangle(jsg::Lock &js, jsg::Ref port) { + disentangle(js); + entangledWith = port.addRef(); + port->entangledWith = JSG_THIS; +} + +void MessagePort::postMessage(jsg::Lock &js, + jsg::Value message, + kj::OneOf, kj::Array> options) {} + +void MessagePort::start(jsg::Lock &js) { + // The start() method steps are to enable this's port message queue, if it is not already enabled. + if (messageQueue == kj::none) { + messageQueue = kj::Vector(); + } +} + +void MessagePort::stop(jsg::Lock &js) {} + +void MessagePort::close(jsg::Lock &js) { + // Set this's [[Detached]] internal slot value to true. + detached = true; + // If this is entangled, disentangle it. + disentangle(js); + // The close event will be fired even if the port is not explicitly closed. + dispatchEvent(js, CloseEvent::constructor()); +} + +MessageChannel::MessageChannel(jsg::Lock &js) + : port1(MessagePort::constructor()), + port2(MessagePort::constructor()) {} + +jsg::Ref MessageChannel::constructor(jsg::Lock &js) { + return jsg::alloc(js); +} + +} // namespace workerd::api diff --git a/src/workerd/api/message-channel.h b/src/workerd/api/message-channel.h new file mode 100644 index 00000000000..79c02a4b404 --- /dev/null +++ b/src/workerd/api/message-channel.h @@ -0,0 +1,116 @@ +// Copyright (c) 2017-2022 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 + +#pragma once + +#include +#include + +#include + +namespace workerd::api { + +// Implements MessagePort web-spec +// Ref: https://html.spec.whatwg.org/multipage/web-messaging.html#message-ports +class MessagePort: public EventTarget { + public: + MessagePort() = default; + ~MessagePort() noexcept(false); + KJ_DISALLOW_COPY(MessagePort); + + static jsg::Ref constructor(); + + struct StructuredSerializeOptions { + kj::Array transfer{}; + + JSG_STRUCT(transfer); + }; + + void postMessage(jsg::Lock& js, + jsg::Value message, + kj::OneOf, kj::Array> options); + void start(jsg::Lock& js); + void stop(jsg::Lock& js); + void close(jsg::Lock& js); + + JSG_RESOURCE_TYPE(MessagePort) { + JSG_NESTED_TYPE(EventTarget); + JSG_METHOD(postMessage); + JSG_METHOD(start); + JSG_METHOD(stop); + JSG_METHOD(close); + } + + // Ref: https://html.spec.whatwg.org/multipage/web-messaging.html#disentangle + void disentangle(jsg::Lock& js); + + // Ref: https://html.spec.whatwg.org/multipage/web-messaging.html#entangle + void entangle(jsg::Lock& js, jsg::Ref port); + + private: + bool detached = false; + + kj::Maybe> onmessage; + kj::Maybe> onmessageerror; + + // Each MessagePort object can be entangled with another (a symmetric relationship) + kj::Maybe> entangledWith{}; + + class Message { + public: + Message() = default; + }; + + bool isMessageQueueEnabled() const { + return messageQueue != kj::none; + } + + // Each MessagePort object also has a task source called the port message queue, + // A port message queue can be enabled or disabled, and is initially disabled. + // Once enabled, a port can never be disabled again + kj::Maybe> messageQueue{}; + + bool hasBeenShipped = false; +}; + +// Implements MessageChannel web-spec +// Ref: https://html.spec.whatwg.org/multipage/web-messaging.html#message-channels +class MessageChannel: public jsg::Object { + public: + explicit MessageChannel(jsg::Lock& js); + + static jsg::Ref constructor(jsg::Lock& js); + + jsg::Ref getPort1() { + return port1.addRef(); + } + + jsg::Ref getPort2() { + return port2.addRef(); + } + + JSG_RESOURCE_TYPE(MessageChannel) { + JSG_READONLY_PROTOTYPE_PROPERTY(port1, getPort1); + JSG_READONLY_PROTOTYPE_PROPERTY(port2, getPort2); + } + + void visitForMemoryInfo(jsg::MemoryTracker& tracker) const { + tracker.trackField("port1", port1); + tracker.trackField("port2", port2); + } + + void visitForGc(jsg::GcVisitor& visitor) { + visitor.visit(port1); + visitor.visit(port2); + } + + private: + jsg::Ref port1; + jsg::Ref port2; +}; + +#define EW_MESSAGE_CHANNEL_ISOLATE_TYPES \ + api::MessageChannel, api::MessagePort, api::MessagePort::StructuredSerializeOptions + +} // namespace workerd::api diff --git a/src/workerd/api/web-socket.h b/src/workerd/api/web-socket.h index 22ffea105dd..cd8bf917695 100644 --- a/src/workerd/api/web-socket.h +++ b/src/workerd/api/web-socket.h @@ -98,64 +98,6 @@ class MessageEvent: public Event { } }; -class CloseEvent: public Event { - public: - CloseEvent(uint code, kj::String reason, bool clean) - : Event("close"), - code(code), - reason(kj::mv(reason)), - clean(clean) {} - CloseEvent(kj::String type, int code, kj::String reason, bool clean) - : Event(kj::mv(type)), - code(code), - reason(kj::mv(reason)), - clean(clean) {} - - struct Initializer { - jsg::Optional code; - jsg::Optional reason; - jsg::Optional wasClean; - - JSG_STRUCT(code, reason, wasClean); - JSG_STRUCT_TS_OVERRIDE(CloseEventInit); - }; - static jsg::Ref constructor(kj::String type, jsg::Optional initializer) { - Initializer init = kj::mv(initializer).orDefault({}); - return jsg::alloc(kj::mv(type), init.code.orDefault(0), - kj::mv(init.reason).orDefault(nullptr), init.wasClean.orDefault(false)); - } - - int getCode() { - return code; - } - kj::StringPtr getReason() { - return reason; - } - bool getWasClean() { - return clean; - } - - JSG_RESOURCE_TYPE(CloseEvent) { - JSG_INHERIT(Event); - - JSG_READONLY_INSTANCE_PROPERTY(code, getCode); - JSG_READONLY_INSTANCE_PROPERTY(reason, getReason); - JSG_READONLY_INSTANCE_PROPERTY(wasClean, getWasClean); - - JSG_TS_ROOT(); - // CloseEvent will be referenced from the `WebSocketEventMap` define - } - - void visitForMemoryInfo(jsg::MemoryTracker& tracker) const { - tracker.trackField("reason", reason); - } - - private: - int code; - kj::String reason; - bool clean; -}; - // The forward declaration is necessary so we can make some // WebSocket methods accessible to WebSocketPair via friend declaration. class WebSocket; @@ -695,8 +637,7 @@ class WebSocket: public EventTarget { }; #define EW_WEBSOCKET_ISOLATE_TYPES \ - api::CloseEvent, api::CloseEvent::Initializer, api::MessageEvent, \ - api::MessageEvent::Initializer, api::WebSocket, api::WebSocketPair, \ + api::MessageEvent, api::MessageEvent::Initializer, api::WebSocket, api::WebSocketPair, \ api::WebSocketPair::PairIterator, \ api::WebSocketPair::PairIterator:: \ Next // The list of websocket.h types that are added to worker.c++'s JSG_DECLARE_ISOLATE_TYPE diff --git a/src/workerd/server/BUILD.bazel b/src/workerd/server/BUILD.bazel index 58037da488b..a61348459c2 100644 --- a/src/workerd/server/BUILD.bazel +++ b/src/workerd/server/BUILD.bazel @@ -117,6 +117,7 @@ wd_cc_library( "//src/workerd/api:html-rewriter", "//src/workerd/api:hyperdrive", "//src/workerd/api:memory-cache", + "//src/workerd/api:message-channel", "//src/workerd/api:pyodide", "//src/workerd/api:r2", "//src/workerd/api:rtti", diff --git a/src/workerd/server/workerd-api.c++ b/src/workerd/server/workerd-api.c++ index b574b84ecb5..d260264b6cd 100644 --- a/src/workerd/server/workerd-api.c++ +++ b/src/workerd/server/workerd-api.c++ @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -103,6 +104,7 @@ JSG_DECLARE_ISOLATE_TYPE(JsgWorkerdIsolate, EW_UNSAFE_ISOLATE_TYPES, EW_MEMORY_CACHE_ISOLATE_TYPES, EW_URL_ISOLATE_TYPES, + EW_MESSAGE_CHANNEL_ISOLATE_TYPES, EW_URL_STANDARD_ISOLATE_TYPES, EW_URLPATTERN_ISOLATE_TYPES, EW_WEBSOCKET_ISOLATE_TYPES,