Skip to content
Merged
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
33 changes: 28 additions & 5 deletions src/workerd/api/events.c++
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include "events.h"

#include "blob.h"
#include "messagechannel.h"

namespace workerd::api {
Expand Down Expand Up @@ -37,7 +38,7 @@ MessageEvent::MessageEvent(jsg::Lock& js,
maybeOrigin(urlForOrigin.map([](auto& url) { return url.getOrigin(); })) {}
MessageEvent::MessageEvent(jsg::Lock& js,
kj::String type,
jsg::JsRef<jsg::JsValue> data,
kj::OneOf<jsg::JsRef<jsg::JsValue>, jsg::Ref<Blob>> data,
kj::String lastEventId,
kj::Maybe<jsg::Ref<MessagePort>> source,
kj::Maybe<jsg::Url&> urlForOrigin)
Expand All @@ -52,8 +53,16 @@ jsg::Ref<MessageEvent> MessageEvent::constructor(
return js.alloc<MessageEvent>(js, kj::mv(type), kj::mv(initializer.data));
}

jsg::JsValue MessageEvent::getData(jsg::Lock& js) {
return data.getHandle(js);
kj::OneOf<jsg::JsValue, jsg::Ref<Blob>> MessageEvent::getData(jsg::Lock& js) {
KJ_SWITCH_ONEOF(data) {
KJ_CASE_ONEOF(jsValue, jsg::JsRef<jsg::JsValue>) {
return jsValue.getHandle(js);
}
KJ_CASE_ONEOF(blob, jsg::Ref<Blob>) {
return blob.addRef();
}
}
KJ_UNREACHABLE;
}

kj::Maybe<kj::ArrayPtr<const char>> MessageEvent::getOrigin() {
Expand All @@ -78,12 +87,26 @@ kj::ArrayPtr<jsg::Ref<MessagePort>> MessageEvent::getPorts() {
}

void MessageEvent::visitForMemoryInfo(jsg::MemoryTracker& tracker) const {
tracker.trackField("data", data);
KJ_SWITCH_ONEOF(data) {
KJ_CASE_ONEOF(jsValue, jsg::JsRef<jsg::JsValue>) {
tracker.trackField("data", jsValue);
}
KJ_CASE_ONEOF(blob, jsg::Ref<Blob>) {
tracker.trackField("data", blob);
}
}
tracker.trackField("source", maybeSource);
}

void MessageEvent::visitForGc(jsg::GcVisitor& visitor) {
visitor.visit(data);
KJ_SWITCH_ONEOF(data) {
KJ_CASE_ONEOF(jsValue, jsg::JsRef<jsg::JsValue>) {
visitor.visit(jsValue);
}
KJ_CASE_ONEOF(blob, jsg::Ref<Blob>) {
visitor.visit(blob);
}
}
visitor.visit(maybeSource);
}

Expand Down
10 changes: 7 additions & 3 deletions src/workerd/api/events.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

namespace workerd::api {

class Blob;

class MessageEvent final: public Event {
public:
MessageEvent(jsg::Lock& js,
Expand All @@ -31,7 +33,7 @@ class MessageEvent final: public Event {

MessageEvent(jsg::Lock& js,
kj::String type,
jsg::JsRef<jsg::JsValue> data,
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);
Expand All @@ -47,7 +49,7 @@ class MessageEvent final: public Event {
static jsg::Ref<MessageEvent> constructor(
jsg::Lock& js, kj::String type, Initializer initializer);

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

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

Expand All @@ -70,12 +72,14 @@ class MessageEvent final: public Event {
JSG_READONLY_INSTANCE_PROPERTY(ports, getPorts);

JSG_TS_ROOT();
JSG_TS_OVERRIDE({ readonly data: any; });
}

void visitForMemoryInfo(jsg::MemoryTracker& tracker) const;

private:
jsg::JsRef<jsg::JsValue> data;
// Blob is used only by web-socket.h/c++
kj::OneOf<jsg::JsRef<jsg::JsValue>, jsg::Ref<Blob>> data;
kj::String lastEventId;
kj::Maybe<jsg::Ref<MessagePort>> maybeSource;
kj::Maybe<kj::Array<const char>> maybeOrigin;
Expand Down
6 changes: 6 additions & 0 deletions src/workerd/api/global-scope.c++
Original file line number Diff line number Diff line change
Expand Up @@ -627,6 +627,12 @@ kj::Promise<void> ServiceWorkerGlobalScope::setHibernatableEventTimeout(

// TODO(cleanup): the hibernatable websocket handler functions here are largely identical – consider
// folding them.
//
// Note: The hibernatable WebSocket message path passes kj::OneOf<kj::String, kj::Array<byte>>
// directly to the webSocketMessage() handler, so binary data is always delivered as ArrayBuffer.
// The WebSocket binaryType property (and the websocket_standard_binary_type compat flag) has no
// effect here — this is by design for the Durable Object handler API, which bypasses the
// normal WebSocket read loop and its Blob/ArrayBuffer dispatch logic.
void ServiceWorkerGlobalScope::sendHibernatableWebSocketMessage(IoContext& context,
kj::OneOf<kj::String, kj::Array<byte>> message,
kj::Maybe<uint32_t> eventTimeoutMs,
Expand Down
2 changes: 1 addition & 1 deletion src/workerd/api/http.c++
Original file line number Diff line number Diff line change
Expand Up @@ -1559,7 +1559,7 @@ jsg::Promise<jsg::Ref<Response>> fetchImplNoOutputLock(jsg::Lock& js,
}
return js.resolvedPromise(makeHttpResponse(js, jsRequest->getMethodEnum(),
kj::mv(urlList), response.statusCode, response.statusText, *response.headers,
newNullInputStream(), js.alloc<WebSocket>(kj::mv(webSocket)),
newNullInputStream(), js.alloc<WebSocket>(js, kj::mv(webSocket)),
jsRequest->getResponseBodyEncoding(), kj::mv(signal)));
}
}
Expand Down
9 changes: 6 additions & 3 deletions src/workerd/api/tests/http-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,9 @@ export const test = {
);
const webSocket = webSocketResponse.webSocket;
assert.notStrictEqual(webSocket, null);
// The server-side WebSocketPair socket's binaryType depends on the compat flag.
const bt = new WebSocketPair()[0].binaryType;
const wsStr = `WebSocket {\n readyState: 1,\n url: null,\n protocol: '',\n extensions: '',\n binaryType: '${bt}'\n }`;
const messagePromise = new Promise((resolve) => {
webSocket.addEventListener('message', (event) => {
assert.strictEqual(
Expand All @@ -239,9 +242,9 @@ export const test = {
cancelable: false,
defaultPrevented: false,
returnValue: true,
currentTarget: WebSocket { readyState: 1, url: null, protocol: '', extensions: '' },
target: WebSocket { readyState: 1, url: null, protocol: '', extensions: '' },
srcElement: WebSocket { readyState: 1, url: null, protocol: '', extensions: '' },
currentTarget: ${wsStr},
target: ${wsStr},
srcElement: ${wsStr},
timeStamp: 0,
isTrusted: true,
cancelBubble: false,
Expand Down
156 changes: 155 additions & 1 deletion src/workerd/api/tests/websocket-constructor-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@
// Licensed under the Apache 2.0 license found in the LICENSE file or at:
// https://opensource.org/licenses/Apache-2.0

import { doesNotThrow, strictEqual, throws } from 'node:assert';
import {
deepStrictEqual,
doesNotThrow,
strictEqual,
throws,
} from 'node:assert';

// Test that WebSocket constructor handles empty protocols array correctly.
// Per the WebSocket spec, an empty protocols array should be valid and equivalent
Expand Down Expand Up @@ -156,3 +161,152 @@ export const closeCodeSpecInvalid = {
}
},
};

// Test that binaryType defaults to "blob" when websocket_standard_binary_type flag is on.
export const binaryTypeDefaultsToBlob = {
async test() {
const ws = new WebSocket('wss://example.com/');
strictEqual(ws.binaryType, 'blob');
ws.close();
},
};

// Test that binaryType on WebSocketPair also defaults to "blob".
export const binaryTypeWebSocketPairDefault = {
async test() {
const pair = new WebSocketPair();
strictEqual(pair[0].binaryType, 'blob');
strictEqual(pair[1].binaryType, 'blob');
// WebSocketPair sockets require accept() before close(), but we only
// need to verify the default value so no cleanup is needed.
},
};

// Test that binaryType setter accepts "arraybuffer" and "blob".
export const binaryTypeSetterValid = {
async test() {
const ws = new WebSocket('wss://example.com/');
strictEqual(ws.binaryType, 'blob');

ws.binaryType = 'arraybuffer';
strictEqual(ws.binaryType, 'arraybuffer');

ws.binaryType = 'blob';
strictEqual(ws.binaryType, 'blob');
ws.close();
},
};

// Test that binaryType setter silently ignores invalid values per spec.
export const binaryTypeSetterInvalid = {
async test() {
const ws = new WebSocket('wss://example.com/');
strictEqual(ws.binaryType, 'blob');

ws.binaryType = 'notBlobOrArrayBuffer';
strictEqual(ws.binaryType, 'blob');

ws.binaryType = '';
strictEqual(ws.binaryType, 'blob');

ws.binaryType = 'arraybuffer';
strictEqual(ws.binaryType, 'arraybuffer');

ws.binaryType = 'BLOB';
strictEqual(ws.binaryType, 'arraybuffer');
ws.close();
},
};

// Test that binary messages are delivered as Blob when binaryType is "blob".
export const binaryMessageDeliveredAsBlob = {
async test() {
const pair = new WebSocketPair();
const [client, server] = pair;
server.accept();

const data = new Uint8Array([1, 2, 3, 4, 5]);
const received = new Promise((resolve) => {
server.addEventListener('message', (event) => resolve(event.data));
});

client.accept();
client.send(data);

const msg = await received;
strictEqual(msg instanceof Blob, true);
deepStrictEqual(new Uint8Array(await msg.arrayBuffer()), data);

client.close();
server.close();
},
};

// Test that binary messages are delivered as ArrayBuffer when binaryType is "arraybuffer".
export const binaryMessageDeliveredAsArrayBuffer = {
async test() {
const pair = new WebSocketPair();
const [client, server] = pair;
server.accept();
server.binaryType = 'arraybuffer';

const data = new Uint8Array([10, 20, 30]);
const received = new Promise((resolve) => {
server.addEventListener('message', (event) => resolve(event.data));
});

client.accept();
client.send(data);

const msg = await received;
strictEqual(msg instanceof ArrayBuffer, true);
deepStrictEqual(new Uint8Array(msg), data);

client.close();
server.close();
},
};

// Test that switching binaryType mid-stream changes delivery format.
export const binaryTypeSwitchMidStream = {
async test() {
const pair = new WebSocketPair();
const [client, server] = pair;
server.accept();
client.accept();

// Default is "blob" with the compat flag.
strictEqual(server.binaryType, 'blob');

// First message: delivered as Blob.
{
const received = new Promise((resolve) => {
server.addEventListener('message', (event) => resolve(event.data), {
once: true,
});
});
client.send(new Uint8Array([1]));
const msg = await received;
strictEqual(msg instanceof Blob, true);
}

// Switch to arraybuffer.
server.binaryType = 'arraybuffer';

// Second message: delivered as ArrayBuffer.
{
const received = new Promise((resolve) => {
server.addEventListener('message', (event) => resolve(event.data), {
once: true,
});
});
client.send(new Uint8Array([2]));
const msg = await received;
strictEqual(msg instanceof ArrayBuffer, true);
deepStrictEqual(new Uint8Array(msg), new Uint8Array([2]));
}

client.close();
server.close();
},
};
2 changes: 1 addition & 1 deletion src/workerd/api/tests/websocket-constructor-test.wd-test
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const unitTests :Workerd.Config = (
modules = [
(name = "worker", esModule = embed "websocket-constructor-test.js")
],
compatibilityFlags = ["nodejs_compat", "websocket_close_reason_byte_limit", "pedantic_wpt"]
compatibilityFlags = ["nodejs_compat", "websocket_close_reason_byte_limit", "pedantic_wpt", "websocket_standard_binary_type"]
)
),
],
Expand Down
Loading