Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 49 additions & 16 deletions src/workerd/api/events.c++
Original file line number Diff line number Diff line change
Expand Up @@ -19,48 +19,76 @@ MessageEvent::MessageEvent(jsg::Lock& js,
const jsg::JsValue& data,
kj::String lastEventId,
kj::Maybe<jsg::Ref<MessagePort>> source,
kj::Maybe<jsg::Url&> urlForOrigin)
: Event(kMessageEventName),
kj::Maybe<kj::Array<const char>> 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<jsg::JsValue> data,
kj::String lastEventId,
kj::Maybe<jsg::Ref<MessagePort>> source,
kj::Maybe<jsg::Url&> urlForOrigin)
: Event(kMessageEventName),
kj::Maybe<kj::Array<const char>> 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<jsg::Ref<MessagePort>> source,
kj::Maybe<jsg::Url&> urlForOrigin)
: Event(kj::mv(type)),
kj::Maybe<kj::Array<const char>> 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::JsRef<jsg::JsValue>, jsg::Ref<Blob>> data,
kj::String lastEventId,
kj::Maybe<jsg::Ref<MessagePort>> source,
kj::Maybe<jsg::Url&> urlForOrigin)
: Event(kj::mv(type)),
kj::Maybe<kj::Array<const char>> 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> MessageEvent::constructor(
jsg::Lock& js, kj::String type, Initializer initializer) {
return js.alloc<MessageEvent>(js, kj::mv(type), kj::mv(initializer.data));
jsg::Lock& js, kj::String type, jsg::Optional<Initializer> maybeInitializer) {
Initializer initializer = kj::mv(maybeInitializer).orDefault({});

// Per the spec, MessageEventInit's data member defaults to null.
kj::OneOf<jsg::JsRef<jsg::JsValue>, jsg::Ref<Blob>> data =
jsg::JsRef<jsg::JsValue>(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<kj::Array<const char>> origin;
KJ_IF_SOME(o, initializer.origin) {
origin = kj::Array<const char>(kj::heapArray<char>(o.asPtr()));
}

return js.alloc<MessageEvent>(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<jsg::JsValue, jsg::Ref<Blob>> MessageEvent::getData(jsg::Lock& js) {
Expand All @@ -75,8 +103,11 @@ kj::OneOf<jsg::JsValue, jsg::Ref<Blob>> MessageEvent::getData(jsg::Lock& js) {
KJ_UNREACHABLE;
}

kj::Maybe<kj::ArrayPtr<const char>> MessageEvent::getOrigin() {
return maybeOrigin.map([](auto& a) -> kj::ArrayPtr<const char> { return a.asPtr(); });
kj::ArrayPtr<const char> 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<const char> {
return a.asPtr();
}).orDefault(nullptr);
}

kj::StringPtr MessageEvent::getLastEventId() {
Expand Down Expand Up @@ -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) {
Expand Down
39 changes: 29 additions & 10 deletions src/workerd/api/events.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,42 +16,61 @@ class MessageEvent final: public Event {
const jsg::JsValue& data,
kj::String lastEventId = kj::String(),
kj::Maybe<jsg::Ref<MessagePort>> source = kj::none,
kj::Maybe<jsg::Url&> urlForOrigin = kj::none);
kj::Maybe<kj::Array<const char>> origin = kj::none,
Event::Init init = {});

MessageEvent(jsg::Lock& js,
jsg::JsRef<jsg::JsValue> data,
kj::String lastEventId = kj::String(),
kj::Maybe<jsg::Ref<MessagePort>> source = kj::none,
kj::Maybe<jsg::Url&> urlForOrigin = kj::none);
kj::Maybe<kj::Array<const char>> origin = kj::none,
Event::Init init = {});

MessageEvent(jsg::Lock& js,
kj::String type,
const jsg::JsValue& data,
kj::String lastEventId = kj::String(),
kj::Maybe<jsg::Ref<MessagePort>> source = kj::none,
kj::Maybe<jsg::Url&> urlForOrigin = kj::none);
kj::Maybe<kj::Array<const char>> origin = kj::none,
Event::Init init = {});

MessageEvent(jsg::Lock& js,
kj::String type,
kj::OneOf<jsg::JsRef<jsg::JsValue>, jsg::Ref<Blob>> data,
kj::String lastEventId = kj::String(),
kj::Maybe<jsg::Ref<MessagePort>> source = kj::none,
kj::Maybe<jsg::Url&> urlForOrigin = kj::none);
kj::Maybe<kj::Array<const char>> origin = kj::none,
Event::Init init = {});

struct Initializer {
jsg::JsRef<jsg::JsValue> data;

JSG_STRUCT(data);
jsg::Optional<jsg::JsRef<jsg::JsValue>> data;
jsg::Optional<jsg::USVString> origin;
jsg::Optional<kj::String> lastEventId;
jsg::Optional<jsg::Ref<MessagePort>> 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<bool> bubbles;
jsg::Optional<bool> cancelable;
jsg::Optional<bool> 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<MessageEvent> constructor(
jsg::Lock& js, kj::String type, Initializer initializer);
jsg::Lock& js, kj::String type, jsg::Optional<Initializer> initializer);

kj::OneOf<jsg::JsValue, jsg::Ref<Blob>> getData(jsg::Lock& js);

kj::Maybe<kj::ArrayPtr<const char>> getOrigin();
kj::ArrayPtr<const char> getOrigin();

kj::StringPtr getLastEventId();

Expand Down
2 changes: 1 addition & 1 deletion src/workerd/api/eventsource.c++
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ void EventSource::notifyMessages(jsg::Lock& js, kj::Array<PendingMessage> messag
kj::String type = kj::mv(message.event).orDefault([]() { return kj::str("message"); });
dispatchEventImpl(js,
js.alloc<MessageEvent>(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
Expand Down
89 changes: 89 additions & 0 deletions src/workerd/api/tests/events-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 9 additions & 3 deletions types/generated-snapshot/experimental/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -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.
*
Expand All @@ -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.
Expand Down
12 changes: 9 additions & 3 deletions types/generated-snapshot/experimental/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -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.
*
Expand All @@ -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.
Expand Down
12 changes: 9 additions & 3 deletions types/generated-snapshot/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -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.
*
Expand All @@ -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.
Expand Down
Loading
Loading