diff --git a/src/workerd/api/events.c++ b/src/workerd/api/events.c++ index ad8a46c453c..d97ecdb8e8a 100644 --- a/src/workerd/api/events.c++ +++ b/src/workerd/api/events.c++ @@ -19,48 +19,76 @@ MessageEvent::MessageEvent(jsg::Lock& js, const jsg::JsValue& data, kj::String lastEventId, kj::Maybe> source, - kj::Maybe urlForOrigin) - : Event(kMessageEventName), + kj::Maybe> origin, + Event::Init init) + : Event(kMessageEventName, kj::mv(init)), data(jsg::JsRef(js, data)), lastEventId(kj::mv(lastEventId)), maybeSource(kj::mv(source)), - maybeOrigin(urlForOrigin.map([](auto& url) { return url.getOrigin(); })) {} + maybeOrigin(kj::mv(origin)) {} MessageEvent::MessageEvent(jsg::Lock& js, jsg::JsRef data, kj::String lastEventId, kj::Maybe> source, - kj::Maybe urlForOrigin) - : Event(kMessageEventName), + kj::Maybe> origin, + Event::Init init) + : Event(kMessageEventName, kj::mv(init)), data(kj::mv(data)), lastEventId(kj::mv(lastEventId)), maybeSource(kj::mv(source)), - maybeOrigin(urlForOrigin.map([](auto& url) { return url.getOrigin(); })) {} + maybeOrigin(kj::mv(origin)) {} MessageEvent::MessageEvent(jsg::Lock& js, kj::String type, const jsg::JsValue& data, kj::String lastEventId, kj::Maybe> source, - kj::Maybe urlForOrigin) - : Event(kj::mv(type)), + kj::Maybe> origin, + Event::Init init) + : Event(kj::mv(type), kj::mv(init)), data(jsg::JsRef(js, kj::mv(data))), lastEventId(kj::mv(lastEventId)), maybeSource(kj::mv(source)), - maybeOrigin(urlForOrigin.map([](auto& url) { return url.getOrigin(); })) {} + maybeOrigin(kj::mv(origin)) {} MessageEvent::MessageEvent(jsg::Lock& js, kj::String type, kj::OneOf, jsg::Ref> data, kj::String lastEventId, kj::Maybe> source, - kj::Maybe urlForOrigin) - : Event(kj::mv(type)), + kj::Maybe> origin, + Event::Init init) + : Event(kj::mv(type), kj::mv(init)), data(kj::mv(data)), lastEventId(kj::mv(lastEventId)), maybeSource(kj::mv(source)), - maybeOrigin(urlForOrigin.map([](auto& url) { return url.getOrigin(); })) {} + maybeOrigin(kj::mv(origin)) {} jsg::Ref MessageEvent::constructor( - jsg::Lock& js, kj::String type, Initializer initializer) { - return js.alloc(js, kj::mv(type), kj::mv(initializer.data)); + jsg::Lock& js, kj::String type, jsg::Optional maybeInitializer) { + Initializer initializer = kj::mv(maybeInitializer).orDefault({}); + + // Per the spec, MessageEventInit's data member defaults to null. + kj::OneOf, jsg::Ref> data = + jsg::JsRef(js, js.null()); + KJ_IF_SOME(d, initializer.data) { + data = kj::mv(d); + } + + // A MessageEvent's origin is "an origin, a string, or null". The origin getter only + // serializes when it holds an actual origin; a string is returned as given. The + // initializer's origin is a USVString, so it is stored verbatim and never parsed. + kj::Maybe> origin; + KJ_IF_SOME(o, initializer.origin) { + origin = kj::Array(kj::heapArray(o.asPtr())); + } + + return js.alloc(js, kj::mv(type), kj::mv(data), + kj::mv(initializer.lastEventId).orDefault(kj::String()), kj::mv(initializer.source), + kj::mv(origin), + Event::Init{ + .bubbles = initializer.bubbles, + .cancelable = initializer.cancelable, + .composed = initializer.composed, + }); } kj::OneOf> MessageEvent::getData(jsg::Lock& js) { @@ -75,8 +103,11 @@ kj::OneOf> MessageEvent::getData(jsg::Lock& js) { KJ_UNREACHABLE; } -kj::Maybe> MessageEvent::getOrigin() { - return maybeOrigin.map([](auto& a) -> kj::ArrayPtr { return a.asPtr(); }); +kj::ArrayPtr MessageEvent::getOrigin() { + // Per the spec, an origin that was never set is reported as the empty string, not null. + return maybeOrigin.map([](auto& a) -> kj::ArrayPtr { + return a.asPtr(); + }).orDefault(nullptr); } kj::StringPtr MessageEvent::getLastEventId() { @@ -106,6 +137,8 @@ void MessageEvent::visitForMemoryInfo(jsg::MemoryTracker& tracker) const { } } tracker.trackField("source", maybeSource); + tracker.trackField("lastEventId", lastEventId); + tracker.trackField("origin", maybeOrigin); } void MessageEvent::visitForGc(jsg::GcVisitor& visitor) { diff --git a/src/workerd/api/events.h b/src/workerd/api/events.h index 2cac81f2aea..c93c51180d8 100644 --- a/src/workerd/api/events.h +++ b/src/workerd/api/events.h @@ -16,42 +16,61 @@ class MessageEvent final: public Event { const jsg::JsValue& data, kj::String lastEventId = kj::String(), kj::Maybe> source = kj::none, - kj::Maybe urlForOrigin = kj::none); + kj::Maybe> origin = kj::none, + Event::Init init = {}); MessageEvent(jsg::Lock& js, jsg::JsRef data, kj::String lastEventId = kj::String(), kj::Maybe> source = kj::none, - kj::Maybe urlForOrigin = kj::none); + kj::Maybe> origin = kj::none, + Event::Init init = {}); MessageEvent(jsg::Lock& js, kj::String type, const jsg::JsValue& data, kj::String lastEventId = kj::String(), kj::Maybe> source = kj::none, - kj::Maybe urlForOrigin = kj::none); + kj::Maybe> origin = kj::none, + Event::Init init = {}); MessageEvent(jsg::Lock& js, kj::String type, kj::OneOf, jsg::Ref> data, kj::String lastEventId = kj::String(), kj::Maybe> source = kj::none, - kj::Maybe urlForOrigin = kj::none); + kj::Maybe> origin = kj::none, + Event::Init init = {}); struct Initializer { - jsg::JsRef data; - - JSG_STRUCT(data); + jsg::Optional> data; + jsg::Optional origin; + jsg::Optional lastEventId; + jsg::Optional> source; + + // Per the spec, MessageEventInit inherits from EventInit. JSG_STRUCT has no + // notion of dictionary inheritance, so EventInit's members are repeated here + // and forwarded on to the Event base class. + jsg::Optional bubbles; + jsg::Optional cancelable; + jsg::Optional composed; + + // Note that the spec also defines a `ports` member. We do not support transferring + // MessagePorts in a MessageEvent (see getPorts() below), so it is omitted here and, + // like any other unrecognized member, is dropped. `ports` therefore always reads + // back as an empty array even when one was supplied. + + JSG_STRUCT(data, origin, lastEventId, source, bubbles, cancelable, composed); JSG_STRUCT_TS_OVERRIDE(MessageEventInit { - data: ArrayBuffer | string; + data?: any; }); }; static jsg::Ref constructor( - jsg::Lock& js, kj::String type, Initializer initializer); + jsg::Lock& js, kj::String type, jsg::Optional initializer); kj::OneOf> getData(jsg::Lock& js); - kj::Maybe> getOrigin(); + kj::ArrayPtr getOrigin(); kj::StringPtr getLastEventId(); diff --git a/src/workerd/api/eventsource.c++ b/src/workerd/api/eventsource.c++ index 21e3e35687c..d8097691839 100644 --- a/src/workerd/api/eventsource.c++ +++ b/src/workerd/api/eventsource.c++ @@ -310,7 +310,7 @@ void EventSource::notifyMessages(jsg::Lock& js, kj::Array messag kj::String type = kj::mv(message.event).orDefault([]() { return kj::str("message"); }); dispatchEventImpl(js, js.alloc(js, kj::mv(type), js.str(data), kj::mv(message.id), - kj::none /** source **/, impl.map([](FetchImpl& i) -> jsg::Url& { return i.url; }))); + kj::none /** source **/, impl.map([](FetchImpl& i) { return i.url.getOrigin(); }))); } }, [&](jsg::Value exception) { // If we end up with an exception being thrown in one of the event handlers, we will diff --git a/src/workerd/api/tests/events-test.js b/src/workerd/api/tests/events-test.js index 58f0a4eb705..2bbb6418b56 100644 --- a/src/workerd/api/tests/events-test.js +++ b/src/workerd/api/tests/events-test.js @@ -434,6 +434,95 @@ export const customEvent = { }, }; +export const messageEvent = { + test() { + // The MessageEvent constructor second argument is optional, and every member + // of MessageEventInit defaults per the spec. + const empty = new MessageEvent('foo'); + strictEqual(empty.type, 'foo'); + strictEqual(empty.data, null); + strictEqual(empty.lastEventId, ''); + strictEqual(empty.origin, ''); + strictEqual(empty.source, null); + deepStrictEqual(empty.ports, []); + + // MessageEventInit's own members are all honored. + const event = new MessageEvent('foo', { + data: { a: 123 }, + lastEventId: '123', + origin: 'https://example.org', + }); + ok(event instanceof Event); + strictEqual(event.type, 'foo'); + deepStrictEqual(event.data, { a: 123 }); + strictEqual(event.lastEventId, '123'); + strictEqual(event.origin, 'https://example.org'); + + // A MessageEvent's origin is "an origin, a string, or null", and the getter only + // serializes when it holds an actual origin. MessageEventInit's origin is a + // USVString, so it is returned exactly as given rather than parsed or normalized. + for (const origin of [ + 'https://example.org/some/path?a=b', + 'HTTPS://EXAMPLE.ORG', + 'https://example.org:443', + 'not a url', + // "null" is the serialization of an opaque origin, so an event carrying one has + // to survive a round trip back through the constructor. + 'null', + '', + ]) { + strictEqual(new MessageEvent('foo', { origin }).origin, origin); + } + + // origin is a non-nullable USVString, so it is never null and a non-string is + // coerced rather than rejected. Only undefined counts as "not provided". + strictEqual(new MessageEvent('foo', { origin: undefined }).origin, ''); + strictEqual(new MessageEvent('foo', { origin: null }).origin, 'null'); + strictEqual(new MessageEvent('foo', { origin: 123 }).origin, '123'); + + // MessageEventInit inherits from EventInit, so those members are honored too. + const bubbling = new MessageEvent('foo', { + data: 'x', + bubbles: true, + cancelable: true, + composed: true, + }); + strictEqual(bubbling.bubbles, true); + strictEqual(bubbling.cancelable, true); + strictEqual(bubbling.composed, true); + + // lastEventId is stringified rather than requiring a string. + strictEqual( + new MessageEvent('foo', { lastEventId: 123 }).lastEventId, + '123' + ); + + // The members survive dispatch, and remain read only. + const target = new EventTarget(); + let dispatchCount = 0; + target.addEventListener('foo', (dispatched) => { + strictEqual(dispatched.lastEventId, '123'); + strictEqual(dispatched.origin, 'https://example.org'); + dispatchCount++; + }); + target.dispatchEvent(event); + strictEqual(dispatchCount, 1); + + throws(() => (event.data = 'nope')); + throws(() => (event.lastEventId = 'nope')); + throws(() => (event.origin = 'nope')); + + // A MessagePort may be used as the source. Anything else is rejected. + // MessageChannel is only exposed globally with a recent enough compatibility + // date, and this test runs under the oldest one too, so guard on it. + if (typeof MessageChannel === 'function') { + const { port1 } = new MessageChannel(); + strictEqual(new MessageEvent('foo', { source: port1 }).source, port1); + } + throws(() => new MessageEvent('foo', { source: 'not a port' })); + }, +}; + export const closeEvent = { test() { // The CloseEvent constructor second argument is optional. Our implementation diff --git a/types/generated-snapshot/experimental/index.d.ts b/types/generated-snapshot/experimental/index.d.ts index 557f4a99a97..9967632403e 100755 --- a/types/generated-snapshot/experimental/index.d.ts +++ b/types/generated-snapshot/experimental/index.d.ts @@ -1700,7 +1700,7 @@ interface ErrorEventErrorEventInit { * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessageEvent) */ declare class MessageEvent extends Event { - constructor(type: string, initializer: MessageEventInit); + constructor(type: string, initializer?: MessageEventInit); /** * The **`data`** read-only property of the MessageEvent interface represents the data sent by the message emitter. * @@ -1712,7 +1712,7 @@ declare class MessageEvent extends Event { * * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessageEvent/origin) */ - readonly origin: string | null; + readonly origin: string; /** * The **`lastEventId`** read-only property of the MessageEvent interface is a string representing a unique ID for the event. * @@ -1733,7 +1733,13 @@ declare class MessageEvent extends Event { readonly ports: MessagePort[]; } interface MessageEventInit { - data: ArrayBuffer | string; + data?: any; + origin?: string; + lastEventId?: string; + source?: MessagePort; + bubbles?: boolean; + cancelable?: boolean; + composed?: boolean; } /** * The **`PromiseRejectionEvent`** interface represents events which are sent to the global script context when JavaScript Promises are rejected. These events are particularly useful for telemetry and debugging purposes. diff --git a/types/generated-snapshot/experimental/index.ts b/types/generated-snapshot/experimental/index.ts index 3f152773310..9ef70628704 100755 --- a/types/generated-snapshot/experimental/index.ts +++ b/types/generated-snapshot/experimental/index.ts @@ -1704,7 +1704,7 @@ export interface ErrorEventErrorEventInit { * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessageEvent) */ export declare class MessageEvent extends Event { - constructor(type: string, initializer: MessageEventInit); + constructor(type: string, initializer?: MessageEventInit); /** * The **`data`** read-only property of the MessageEvent interface represents the data sent by the message emitter. * @@ -1716,7 +1716,7 @@ export declare class MessageEvent extends Event { * * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessageEvent/origin) */ - readonly origin: string | null; + readonly origin: string; /** * The **`lastEventId`** read-only property of the MessageEvent interface is a string representing a unique ID for the event. * @@ -1737,7 +1737,13 @@ export declare class MessageEvent extends Event { readonly ports: MessagePort[]; } export interface MessageEventInit { - data: ArrayBuffer | string; + data?: any; + origin?: string; + lastEventId?: string; + source?: MessagePort; + bubbles?: boolean; + cancelable?: boolean; + composed?: boolean; } /** * The **`PromiseRejectionEvent`** interface represents events which are sent to the global script context when JavaScript Promises are rejected. These events are particularly useful for telemetry and debugging purposes. diff --git a/types/generated-snapshot/index.d.ts b/types/generated-snapshot/index.d.ts index 18bf68a6767..30eed1d79c5 100755 --- a/types/generated-snapshot/index.d.ts +++ b/types/generated-snapshot/index.d.ts @@ -1675,7 +1675,7 @@ interface ErrorEventErrorEventInit { * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessageEvent) */ declare class MessageEvent extends Event { - constructor(type: string, initializer: MessageEventInit); + constructor(type: string, initializer?: MessageEventInit); /** * The **`data`** read-only property of the MessageEvent interface represents the data sent by the message emitter. * @@ -1687,7 +1687,7 @@ declare class MessageEvent extends Event { * * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessageEvent/origin) */ - readonly origin: string | null; + readonly origin: string; /** * The **`lastEventId`** read-only property of the MessageEvent interface is a string representing a unique ID for the event. * @@ -1708,7 +1708,13 @@ declare class MessageEvent extends Event { readonly ports: MessagePort[]; } interface MessageEventInit { - data: ArrayBuffer | string; + data?: any; + origin?: string; + lastEventId?: string; + source?: MessagePort; + bubbles?: boolean; + cancelable?: boolean; + composed?: boolean; } /** * The **`PromiseRejectionEvent`** interface represents events which are sent to the global script context when JavaScript Promises are rejected. These events are particularly useful for telemetry and debugging purposes. diff --git a/types/generated-snapshot/index.ts b/types/generated-snapshot/index.ts index beed08a500f..7545d844e79 100755 --- a/types/generated-snapshot/index.ts +++ b/types/generated-snapshot/index.ts @@ -1679,7 +1679,7 @@ export interface ErrorEventErrorEventInit { * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessageEvent) */ export declare class MessageEvent extends Event { - constructor(type: string, initializer: MessageEventInit); + constructor(type: string, initializer?: MessageEventInit); /** * The **`data`** read-only property of the MessageEvent interface represents the data sent by the message emitter. * @@ -1691,7 +1691,7 @@ export declare class MessageEvent extends Event { * * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessageEvent/origin) */ - readonly origin: string | null; + readonly origin: string; /** * The **`lastEventId`** read-only property of the MessageEvent interface is a string representing a unique ID for the event. * @@ -1712,7 +1712,13 @@ export declare class MessageEvent extends Event { readonly ports: MessagePort[]; } export interface MessageEventInit { - data: ArrayBuffer | string; + data?: any; + origin?: string; + lastEventId?: string; + source?: MessagePort; + bubbles?: boolean; + cancelable?: boolean; + composed?: boolean; } /** * The **`PromiseRejectionEvent`** interface represents events which are sent to the global script context when JavaScript Promises are rejected. These events are particularly useful for telemetry and debugging purposes.