1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-12-21 23:04:45 -05:00

fix(ext/websocket): correctly order messages when sending blobs (#24133)

Previously the asynchronous read of the blob would not block sends that
are started later. We now do this, but in such a way as to not regress
performance in the common case of not using `Blob`.
This commit is contained in:
Luca Casonato 2024-06-08 18:36:43 +02:00 committed by GitHub
parent c1f23c5788
commit 585ba28d47
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 84 additions and 16 deletions

View file

@ -25,6 +25,8 @@ const {
ArrayBufferIsView,
ArrayPrototypeJoin,
ArrayPrototypeMap,
ArrayPrototypePush,
ArrayPrototypeShift,
ArrayPrototypeSome,
ErrorPrototypeToString,
ObjectDefineProperties,
@ -41,7 +43,6 @@ const {
SymbolFor,
SymbolIterator,
TypedArrayPrototypeGetByteLength,
Uint8Array,
} = primordials;
import { URL } from "ext:deno_url/00_url.js";
@ -111,11 +112,14 @@ const _extensions = Symbol("[[extensions]]");
const _protocol = Symbol("[[protocol]]");
const _binaryType = Symbol("[[binaryType]]");
const _eventLoop = Symbol("[[eventLoop]]");
const _sendQueue = Symbol("[[sendQueue]]");
const _queueSend = Symbol("[[queueSend]]");
const _server = Symbol("[[server]]");
const _idleTimeoutDuration = Symbol("[[idleTimeout]]");
const _idleTimeoutTimeout = Symbol("[[idleTimeoutTimeout]]");
const _serverHandleIdleTimeout = Symbol("[[serverHandleIdleTimeout]]");
class WebSocket extends EventTarget {
constructor(url, protocols = []) {
super();
@ -129,6 +133,8 @@ class WebSocket extends EventTarget {
this[_binaryType] = "blob";
this[_idleTimeoutDuration] = 0;
this[_idleTimeoutTimeout] = undefined;
this[_sendQueue] = [];
const prefix = "Failed to construct 'WebSocket'";
webidl.requiredArguments(arguments.length, 1, prefix);
url = webidl.converters.USVString(url, prefix, "Argument 1");
@ -326,22 +332,26 @@ class WebSocket extends EventTarget {
throw new DOMException("readyState not OPEN", "InvalidStateError");
}
if (ArrayBufferIsView(data)) {
op_ws_send_binary(this[_rid], data);
} else if (isArrayBuffer(data)) {
op_ws_send_binary(this[_rid], new Uint8Array(data));
} else if (ObjectPrototypeIsPrototypeOf(BlobPrototype, data)) {
PromisePrototypeThen(
// deno-lint-ignore prefer-primordials
data.slice().arrayBuffer(),
(ab) => op_ws_send_binary_ab(this[_rid], ab),
);
if (this[_sendQueue].length === 0) {
// Fast path if the send queue is empty, for example when only synchronous
// data is being sent.
if (ArrayBufferIsView(data)) {
op_ws_send_binary(this[_rid], data);
} else if (isArrayBuffer(data)) {
op_ws_send_binary_ab(this[_rid], data);
} else if (ObjectPrototypeIsPrototypeOf(BlobPrototype, data)) {
this[_queueSend](data);
} else {
const string = String(data);
op_ws_send_text(
this[_rid],
string,
);
}
} else {
const string = String(data);
op_ws_send_text(
this[_rid],
string,
);
// Slower path if the send queue is not empty, for example when sending
// asynchronous data like a Blob.
this[_queueSend](data);
}
}
@ -508,6 +518,38 @@ class WebSocket extends EventTarget {
}
}
async [_queueSend](data) {
const queue = this[_sendQueue];
ArrayPrototypePush(queue, data);
if (queue.length > 1) {
// There is already a send in progress, so we just push to the queue
// and let that task handle sending of this data.
return;
}
while (queue.length > 0) {
const data = queue[0];
if (ArrayBufferIsView(data)) {
op_ws_send_binary(this[_rid], data);
} else if (isArrayBuffer(data)) {
op_ws_send_binary_ab(this[_rid], data);
} else if (ObjectPrototypeIsPrototypeOf(BlobPrototype, data)) {
// deno-lint-ignore prefer-primordials
const ab = await data.slice().arrayBuffer();
op_ws_send_binary_ab(this[_rid], ab);
} else {
const string = String(data);
op_ws_send_text(
this[_rid],
string,
);
}
ArrayPrototypeShift(queue);
}
}
[_serverHandleIdleTimeout]() {
if (this[_idleTimeoutDuration]) {
clearTimeout(this[_idleTimeoutTimeout]);
@ -608,6 +650,7 @@ function createWebSocketBranded() {
socket[_binaryType] = "arraybuffer";
socket[_idleTimeoutDuration] = 0;
socket[_idleTimeoutTimeout] = undefined;
socket[_sendQueue] = [];
return socket;
}

View file

@ -706,6 +706,31 @@ Deno.test("echo arraybuffer with binaryType arraybuffer", async () => {
await promise;
});
Deno.test("echo blob mixed with string", async () => {
const { promise, resolve } = Promise.withResolvers<void>();
const ws = new WebSocket("ws://localhost:4242");
ws.binaryType = "arraybuffer";
const blob = new Blob(["foo"]);
ws.onerror = () => fail();
ws.onopen = () => {
ws.send(blob);
ws.send("bar");
};
const messages: (ArrayBuffer | string)[] = [];
ws.onmessage = (e) => {
messages.push(e.data);
if (messages.length === 2) {
assertEquals(messages[0], new Uint8Array([102, 111, 111]).buffer);
assertEquals(messages[1], "bar");
ws.close();
}
};
ws.onclose = () => {
resolve();
};
await promise;
});
Deno.test("Event Handlers order", async () => {
const { promise, resolve } = Promise.withResolvers<void>();
const ws = new WebSocket("ws://localhost:4242");