diff options
author | 2023-07-13 09:39:43 -0700 | |
---|---|---|
committer | 2023-07-13 09:39:43 -0700 | |
commit | 9eb8eea2a81be6a20abb62544dc54a35ff4173a5 (patch) | |
tree | 63ab7bc037477b0db836067ac6c49c273d251353 /src | |
parent | 04b4157232006c882cdd693f566b31945618b924 (diff) | |
download | bun-9eb8eea2a81be6a20abb62544dc54a35ff4173a5.tar.gz bun-9eb8eea2a81be6a20abb62544dc54a35ff4173a5.tar.zst bun-9eb8eea2a81be6a20abb62544dc54a35ff4173a5.zip |
Implement `ping()`, `pong()`, `terminate()` for WebSocket client and server (#3257)
Diffstat (limited to 'src')
-rw-r--r-- | src/bun.js/api/server.classes.ts | 21 | ||||
-rw-r--r-- | src/bun.js/api/server.zig | 298 | ||||
-rw-r--r-- | src/bun.js/bindings/JSSink.cpp | 2 | ||||
-rw-r--r-- | src/bun.js/bindings/JSSink.h | 2 | ||||
-rw-r--r-- | src/bun.js/bindings/JSSinkLookupTable.h | 2 | ||||
-rw-r--r-- | src/bun.js/bindings/ZigGeneratedClasses.cpp | 95 | ||||
-rw-r--r-- | src/bun.js/bindings/generated_classes.zig | 9 | ||||
-rw-r--r-- | src/bun.js/bindings/headers.h | 136 | ||||
-rw-r--r-- | src/bun.js/bindings/headers.zig | 10 | ||||
-rw-r--r-- | src/bun.js/bindings/webcore/JSWebSocket.cpp | 201 | ||||
-rw-r--r-- | src/bun.js/bindings/webcore/WebSocket.cpp | 332 | ||||
-rw-r--r-- | src/bun.js/bindings/webcore/WebSocket.h | 42 | ||||
-rw-r--r-- | src/bun.js/javascript.zig | 2 | ||||
-rw-r--r-- | src/http/websocket_http_client.zig | 125 | ||||
-rw-r--r-- | src/js/out/modules/thirdparty/ws.js | 123 | ||||
-rw-r--r-- | src/js/thirdparty/ws.js | 162 |
16 files changed, 1229 insertions, 333 deletions
diff --git a/src/bun.js/api/server.classes.ts b/src/bun.js/api/server.classes.ts index b3d174957..3aaea871f 100644 --- a/src/bun.js/api/server.classes.ts +++ b/src/bun.js/api/server.classes.ts @@ -45,7 +45,6 @@ export default [ args: ["JSUint8Array", "bool"], }, }, - publishText: { fn: "publishText", length: 2, @@ -62,10 +61,21 @@ export default [ args: ["JSString", "JSUint8Array"], }, }, - + ping: { + fn: "ping", + length: 1, + }, + pong: { + fn: "pong", + length: 1, + }, close: { fn: "close", - length: 1, + length: 3, + }, + terminate: { + fn: "terminate", + length: 0, }, cork: { fn: "cork", @@ -103,11 +113,6 @@ export default [ fn: "isSubscribed", length: 1, }, - - // topics: { - // getter: "getTopics", - // }, - remoteAddress: { getter: "getRemoteAddress", cache: true, diff --git a/src/bun.js/api/server.zig b/src/bun.js/api/server.zig index 9625ff693..63e83d9bf 100644 --- a/src/bun.js/api/server.zig +++ b/src/bun.js/api/server.zig @@ -2976,11 +2976,11 @@ pub const WebSocketServer = struct { globalObject: *JSC.JSGlobalObject = undefined, handler: WebSocketServer.Handler = .{}, - maxPayloadLength: u32 = 1024 * 1024 * 16, + maxPayloadLength: u32 = 1024 * 1024 * 16, // 16MB maxLifetime: u16 = 0, - idleTimeout: u16 = 120, + idleTimeout: u16 = 120, // 2 minutes compression: i32 = 0, - backpressureLimit: u32 = 1024 * 1024 * 16, + backpressureLimit: u32 = 1024 * 1024 * 16, // 16MB sendPingsAutomatically: bool = true, resetIdleTimeoutOnSend: bool = true, closeOnBackpressureLimit: bool = false, @@ -2991,6 +2991,8 @@ pub const WebSocketServer = struct { onClose: JSC.JSValue = .zero, onDrain: JSC.JSValue = .zero, onError: JSC.JSValue = .zero, + onPing: JSC.JSValue = .zero, + onPong: JSC.JSValue = .zero, app: ?*anyopaque = null, @@ -3005,52 +3007,80 @@ pub const WebSocketServer = struct { pub fn fromJS(globalObject: *JSC.JSGlobalObject, object: JSC.JSValue) ?Handler { var handler = Handler{ .globalObject = globalObject }; + var vm = globalObject.vm(); + var valid = false; + if (object.getTruthy(globalObject, "message")) |message| { - if (!message.isCallable(globalObject.vm())) { + if (!message.isCallable(vm)) { globalObject.throwInvalidArguments("websocket expects a function for the message option", .{}); return null; } handler.onMessage = message; message.ensureStillAlive(); + valid = true; } if (object.getTruthy(globalObject, "open")) |open| { - if (!open.isCallable(globalObject.vm())) { + if (!open.isCallable(vm)) { globalObject.throwInvalidArguments("websocket expects a function for the open option", .{}); return null; } handler.onOpen = open; open.ensureStillAlive(); + valid = true; } if (object.getTruthy(globalObject, "close")) |close| { - if (!close.isCallable(globalObject.vm())) { + if (!close.isCallable(vm)) { globalObject.throwInvalidArguments("websocket expects a function for the close option", .{}); return null; } handler.onClose = close; close.ensureStillAlive(); + valid = true; } if (object.getTruthy(globalObject, "drain")) |drain| { - if (!drain.isCallable(globalObject.vm())) { + if (!drain.isCallable(vm)) { globalObject.throwInvalidArguments("websocket expects a function for the drain option", .{}); return null; } handler.onDrain = drain; drain.ensureStillAlive(); + valid = true; + } + + if (object.getTruthy(globalObject, "error")) |cb| { + if (!cb.isCallable(vm)) { + globalObject.throwInvalidArguments("websocket expects a function for the error option", .{}); + return null; + } + handler.onError = cb; + cb.ensureStillAlive(); + valid = true; + } + + if (object.getTruthy(globalObject, "ping")) |cb| { + if (!cb.isCallable(vm)) { + globalObject.throwInvalidArguments("websocket expects a function for the ping option", .{}); + return null; + } + handler.onPing = cb; + cb.ensureStillAlive(); + valid = true; } - if (object.getTruthy(globalObject, "onError")) |onError| { - if (!onError.isCallable(globalObject.vm())) { - globalObject.throwInvalidArguments("websocket expects a function for the onError option", .{}); + if (object.getTruthy(globalObject, "pong")) |cb| { + if (!cb.isCallable(vm)) { + globalObject.throwInvalidArguments("websocket expects a function for the pong option", .{}); return null; } - handler.onError = onError; - onError.ensureStillAlive(); + handler.onPong = cb; + cb.ensureStillAlive(); + valid = true; } - if (handler.onMessage != .zero or handler.onOpen != .zero) + if (valid) return handler; return null; @@ -3062,6 +3092,8 @@ pub const WebSocketServer = struct { this.onClose.protect(); this.onDrain.protect(); this.onError.protect(); + this.onPing.protect(); + this.onPong.protect(); } pub fn unprotect(this: Handler) void { @@ -3070,6 +3102,8 @@ pub const WebSocketServer = struct { this.onClose.unprotect(); this.onDrain.unprotect(); this.onError.unprotect(); + this.onPing.unprotect(); + this.onPong.unprotect(); } }; @@ -3197,6 +3231,7 @@ pub const WebSocketServer = struct { server.maxPayloadLength = @intCast(u32, @max(value.toInt64(), 0)); } } + if (object.get(globalObject, "idleTimeout")) |value| { if (!value.isUndefinedOrNull()) { if (!value.isAnyInt()) { @@ -3204,7 +3239,17 @@ pub const WebSocketServer = struct { return null; } - server.idleTimeout = value.to(u16); + var idleTimeout = @intCast(u16, @truncate(u32, @max(value.toInt64(), 0))); + if (idleTimeout > 960) { + globalObject.throwInvalidArguments("websocket expects idleTimeout to be 960 or less", .{}); + return null; + } else if (idleTimeout > 0) { + // uws does not allow idleTimeout to be between (0, 8], + // since its timer is not that accurate, therefore round up. + idleTimeout = @max(idleTimeout, 8); + } + + server.idleTimeout = idleTimeout; } } if (object.get(globalObject, "backpressureLimit")) |value| { @@ -3217,16 +3262,6 @@ pub const WebSocketServer = struct { server.backpressureLimit = @intCast(u32, @max(value.toInt64(), 0)); } } - // if (object.get(globalObject, "sendPings")) |value| { - // if (!value.isUndefinedOrNull()) { - // if (!value.isBoolean()) { - // globalObject.throwInvalidArguments("websocket expects sendPings to be a boolean", .{}); - // return null; - // } - - // server.sendPings = value.toBoolean(); - // } - // } if (object.get(globalObject, "closeOnBackpressureLimit")) |value| { if (!value.isUndefinedOrNull()) { @@ -3239,6 +3274,17 @@ pub const WebSocketServer = struct { } } + if (object.get(globalObject, "sendPings")) |value| { + if (!value.isUndefinedOrNull()) { + if (!value.isBoolean()) { + globalObject.throwInvalidArguments("websocket expects sendPings to be a boolean", .{}); + return null; + } + + server.sendPingsAutomatically = value.toBoolean(); + } + } + if (object.get(globalObject, "publishToSelf")) |value| { if (!value.isUndefinedOrNull()) { if (!value.isBoolean()) { @@ -3466,12 +3512,79 @@ pub const ServerWebSocket = struct { } } } - pub fn onPing(_: *ServerWebSocket, _: uws.AnyWebSocket, _: []const u8) void { - log("onPing", .{}); + + pub fn onPing(this: *ServerWebSocket, _: uws.AnyWebSocket, data: []const u8) void { + log("onPing: {s}", .{data}); + + var handler = this.handler; + var cb = handler.onPing; + if (cb.isEmptyOrUndefinedOrNull()) return; + + var globalThis = handler.globalObject; + const result = cb.call( + globalThis, + &[_]JSC.JSValue{ this.this_value, if (this.binary_type == .Buffer) + JSC.ArrayBuffer.create( + globalThis, + data, + .Buffer, + ) + else if (this.binary_type == .Uint8Array) + JSC.ArrayBuffer.create( + globalThis, + data, + .Uint8Array, + ) + else + JSC.ArrayBuffer.create( + globalThis, + data, + .ArrayBuffer, + ) }, + ); + + if (result.toError()) |err| { + log("onPing error", .{}); + handler.globalObject.bunVM().runErrorHandler(err, null); + } } - pub fn onPong(_: *ServerWebSocket, _: uws.AnyWebSocket, _: []const u8) void { - log("onPong", .{}); + + pub fn onPong(this: *ServerWebSocket, _: uws.AnyWebSocket, data: []const u8) void { + log("onPong: {s}", .{data}); + + var handler = this.handler; + var cb = handler.onPong; + if (cb.isEmptyOrUndefinedOrNull()) return; + + var globalThis = handler.globalObject; + const result = cb.call( + globalThis, + &[_]JSC.JSValue{ this.this_value, if (this.binary_type == .Buffer) + JSC.ArrayBuffer.create( + globalThis, + data, + .Buffer, + ) + else if (this.binary_type == .Uint8Array) + JSC.ArrayBuffer.create( + globalThis, + data, + .Uint8Array, + ) + else + JSC.ArrayBuffer.create( + globalThis, + data, + .ArrayBuffer, + ) }, + ); + + if (result.toError()) |err| { + log("onPong error", .{}); + handler.globalObject.bunVM().runErrorHandler(err, null); + } } + pub fn onClose(this: *ServerWebSocket, _: uws.AnyWebSocket, code: i32, message: []const u8) void { log("onClose", .{}); var handler = this.handler; @@ -3483,10 +3596,12 @@ pub const ServerWebSocket = struct { } } - if (handler.onClose != .zero) { + if (!handler.onClose.isEmptyOrUndefinedOrNull()) { + var str = ZigString.init(message); + str.markUTF8(); const result = handler.onClose.call( handler.globalObject, - &[_]JSC.JSValue{ this.this_value, JSValue.jsNumber(code), ZigString.init(message).toValueGC(handler.globalObject) }, + &[_]JSC.JSValue{ this.this_value, JSValue.jsNumber(code), str.toValueGC(handler.globalObject) }, ); if (result.toError()) |err| { @@ -4056,6 +4171,95 @@ pub const ServerWebSocket = struct { } } + pub fn ping( + this: *ServerWebSocket, + globalThis: *JSC.JSGlobalObject, + callframe: *JSC.CallFrame, + ) callconv(.C) JSValue { + return sendPing(this, globalThis, callframe, "ping", .ping); + } + + pub fn pong( + this: *ServerWebSocket, + globalThis: *JSC.JSGlobalObject, + callframe: *JSC.CallFrame, + ) callconv(.C) JSValue { + return sendPing(this, globalThis, callframe, "pong", .pong); + } + + inline fn sendPing( + this: *ServerWebSocket, + globalThis: *JSC.JSGlobalObject, + callframe: *JSC.CallFrame, + comptime name: string, + comptime opcode: uws.Opcode, + ) JSValue { + const args = callframe.arguments(2); + + if (this.closed) { + return JSValue.jsNumber(0); + } + + if (args.len > 0) { + var value = args.ptr[0]; + if (value.asArrayBuffer(globalThis)) |data| { + var buffer = data.slice(); + + switch (this.websocket.send(buffer, opcode, false, true)) { + .backpressure => { + log("{s}() backpressure ({d} bytes)", .{ name, buffer.len }); + return JSValue.jsNumber(-1); + }, + .success => { + log("{s}() success ({d} bytes)", .{ name, buffer.len }); + return JSValue.jsNumber(buffer.len); + }, + .dropped => { + log("{s}() dropped ({d} bytes)", .{ name, buffer.len }); + return JSValue.jsNumber(0); + }, + } + } else if (value.isString()) { + var string_value = value.toString(globalThis).toSlice(globalThis, bun.default_allocator); + defer string_value.deinit(); + var buffer = string_value.slice(); + + switch (this.websocket.send(buffer, opcode, false, true)) { + .backpressure => { + log("{s}() backpressure ({d} bytes)", .{ name, buffer.len }); + return JSValue.jsNumber(-1); + }, + .success => { + log("{s}() success ({d} bytes)", .{ name, buffer.len }); + return JSValue.jsNumber(buffer.len); + }, + .dropped => { + log("{s}() dropped ({d} bytes)", .{ name, buffer.len }); + return JSValue.jsNumber(0); + }, + } + } else { + globalThis.throwPretty("{s} requires a string or BufferSource", .{name}); + return .zero; + } + } + + switch (this.websocket.send(&.{}, opcode, false, true)) { + .backpressure => { + log("{s}() backpressure ({d} bytes)", .{ name, 0 }); + return JSValue.jsNumber(-1); + }, + .success => { + log("{s}() success ({d} bytes)", .{ name, 0 }); + return JSValue.jsNumber(0); + }, + .dropped => { + log("{s}() dropped ({d} bytes)", .{ name, 0 }); + return JSValue.jsNumber(0); + }, + } + } + pub fn getData( _: *ServerWebSocket, _: *JSC.JSGlobalObject, @@ -4096,26 +4300,38 @@ pub const ServerWebSocket = struct { log("close()", .{}); if (this.closed) { - return JSValue.jsUndefined(); + return .undefined; } - if (!this.opened) { - globalThis.throw("Calling close() inside open() is not supported. Consider changing your upgrade() callback instead", .{}); - return .zero; - } this.closed = true; const code = if (args.len > 0) args.ptr[0].toInt32() else @as(i32, 1000); var message_value = if (args.len > 1) args.ptr[1].toSlice(globalThis, bun.default_allocator) else ZigString.Slice.empty; defer message_value.deinit(); - if (code > 1000 or message_value.len > 0) { - this.websocket.end(code, message_value.slice()); - } else { - this.this_value.unprotect(); - this.websocket.close(); + + this.websocket.end(code, message_value.slice()); + return .undefined; + } + + pub fn terminate( + this: *ServerWebSocket, + globalThis: *JSC.JSGlobalObject, + callframe: *JSC.CallFrame, + ) callconv(.C) JSValue { + _ = globalThis; + const args = callframe.arguments(2); + _ = args; + log("terminate()", .{}); + + if (this.closed) { + return .undefined; } - return JSValue.jsUndefined(); + this.closed = true; + this.this_value.unprotect(); + this.websocket.close(); + + return .undefined; } pub fn getBinaryType( diff --git a/src/bun.js/bindings/JSSink.cpp b/src/bun.js/bindings/JSSink.cpp index ed2554dc7..8be4fe95b 100644 --- a/src/bun.js/bindings/JSSink.cpp +++ b/src/bun.js/bindings/JSSink.cpp @@ -1,6 +1,6 @@ // AUTO-GENERATED FILE. DO NOT EDIT. -// Generated by 'make generate-sink' at 2023-07-06T14:22:07.346Z +// Generated by 'make generate-sink' at 2023-07-09T17:58:51.559Z // To regenerate this file, run: // // make generate-sink diff --git a/src/bun.js/bindings/JSSink.h b/src/bun.js/bindings/JSSink.h index 386554ebb..3826ef696 100644 --- a/src/bun.js/bindings/JSSink.h +++ b/src/bun.js/bindings/JSSink.h @@ -1,6 +1,6 @@ // AUTO-GENERATED FILE. DO NOT EDIT. -// Generated by 'make generate-sink' at 2023-07-06T14:22:07.345Z +// Generated by 'make generate-sink' at 2023-07-09T17:58:51.558Z // #pragma once diff --git a/src/bun.js/bindings/JSSinkLookupTable.h b/src/bun.js/bindings/JSSinkLookupTable.h index e4ed81629..7ff4c3f9c 100644 --- a/src/bun.js/bindings/JSSinkLookupTable.h +++ b/src/bun.js/bindings/JSSinkLookupTable.h @@ -1,4 +1,4 @@ -// Automatically generated from src/bun.js/bindings/JSSink.cpp using /home/cirospaciari/Repos/bun/src/bun.js/WebKit/Source/JavaScriptCore/create_hash_table. DO NOT EDIT! +// Automatically generated from src/bun.js/bindings/JSSink.cpp using /Users/ashcon/Desktop/code/bun/src/bun.js/WebKit/Source/JavaScriptCore/create_hash_table. DO NOT EDIT! diff --git a/src/bun.js/bindings/ZigGeneratedClasses.cpp b/src/bun.js/bindings/ZigGeneratedClasses.cpp index b4d672328..ce82dc1f1 100644 --- a/src/bun.js/bindings/ZigGeneratedClasses.cpp +++ b/src/bun.js/bindings/ZigGeneratedClasses.cpp @@ -14587,6 +14587,12 @@ JSC_DECLARE_HOST_FUNCTION(ServerWebSocketPrototype__getBufferedAmountCallback); extern "C" EncodedJSValue ServerWebSocketPrototype__isSubscribed(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); JSC_DECLARE_HOST_FUNCTION(ServerWebSocketPrototype__isSubscribedCallback); +extern "C" EncodedJSValue ServerWebSocketPrototype__ping(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); +JSC_DECLARE_HOST_FUNCTION(ServerWebSocketPrototype__pingCallback); + +extern "C" EncodedJSValue ServerWebSocketPrototype__pong(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); +JSC_DECLARE_HOST_FUNCTION(ServerWebSocketPrototype__pongCallback); + extern "C" EncodedJSValue ServerWebSocketPrototype__publish(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); JSC_DECLARE_HOST_FUNCTION(ServerWebSocketPrototype__publishCallback); @@ -14686,6 +14692,9 @@ JSC_DEFINE_JIT_OPERATION(ServerWebSocketPrototype__sendTextWithoutTypeChecksWrap extern "C" EncodedJSValue ServerWebSocketPrototype__subscribe(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); JSC_DECLARE_HOST_FUNCTION(ServerWebSocketPrototype__subscribeCallback); +extern "C" EncodedJSValue ServerWebSocketPrototype__terminate(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); +JSC_DECLARE_HOST_FUNCTION(ServerWebSocketPrototype__terminateCallback); + extern "C" EncodedJSValue ServerWebSocketPrototype__unsubscribe(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); JSC_DECLARE_HOST_FUNCTION(ServerWebSocketPrototype__unsubscribeCallback); @@ -14693,11 +14702,13 @@ STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSServerWebSocketPrototype, JSServerWebSocke static const HashTableValue JSServerWebSocketPrototypeTableValues[] = { { "binaryType"_s, static_cast<unsigned>(JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::GetterSetterType, ServerWebSocketPrototype__binaryTypeGetterWrap, ServerWebSocketPrototype__binaryTypeSetterWrap } }, - { "close"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ServerWebSocketPrototype__closeCallback, 1 } }, + { "close"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ServerWebSocketPrototype__closeCallback, 3 } }, { "cork"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ServerWebSocketPrototype__corkCallback, 1 } }, { "data"_s, static_cast<unsigned>(JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::GetterSetterType, ServerWebSocketPrototype__dataGetterWrap, ServerWebSocketPrototype__dataSetterWrap } }, { "getBufferedAmount"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ServerWebSocketPrototype__getBufferedAmountCallback, 0 } }, { "isSubscribed"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ServerWebSocketPrototype__isSubscribedCallback, 1 } }, + { "ping"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ServerWebSocketPrototype__pingCallback, 1 } }, + { "pong"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ServerWebSocketPrototype__pongCallback, 1 } }, { "publish"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ServerWebSocketPrototype__publishCallback, 3 } }, { "publishBinary"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | JSC::PropertyAttribute::DOMJITFunction | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::DOMJITFunctionType, ServerWebSocketPrototype__publishBinaryCallback, &DOMJITSignatureForServerWebSocketPrototype__publishBinary } }, { "publishText"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | JSC::PropertyAttribute::DOMJITFunction | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::DOMJITFunctionType, ServerWebSocketPrototype__publishTextCallback, &DOMJITSignatureForServerWebSocketPrototype__publishText } }, @@ -14707,6 +14718,7 @@ static const HashTableValue JSServerWebSocketPrototypeTableValues[] = { { "sendBinary"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | JSC::PropertyAttribute::DOMJITFunction | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::DOMJITFunctionType, ServerWebSocketPrototype__sendBinaryCallback, &DOMJITSignatureForServerWebSocketPrototype__sendBinary } }, { "sendText"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | JSC::PropertyAttribute::DOMJITFunction | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::DOMJITFunctionType, ServerWebSocketPrototype__sendTextCallback, &DOMJITSignatureForServerWebSocketPrototype__sendText } }, { "subscribe"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ServerWebSocketPrototype__subscribeCallback, 1 } }, + { "terminate"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ServerWebSocketPrototype__terminateCallback, 0 } }, { "unsubscribe"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, ServerWebSocketPrototype__unsubscribeCallback, 1 } } }; @@ -14897,6 +14909,60 @@ JSC_DEFINE_HOST_FUNCTION(ServerWebSocketPrototype__isSubscribedCallback, (JSGlob return ServerWebSocketPrototype__isSubscribed(thisObject->wrapped(), lexicalGlobalObject, callFrame); } +JSC_DEFINE_HOST_FUNCTION(ServerWebSocketPrototype__pingCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = lexicalGlobalObject->vm(); + + JSServerWebSocket* thisObject = jsDynamicCast<JSServerWebSocket*>(callFrame->thisValue()); + + if (UNLIKELY(!thisObject)) { + auto throwScope = DECLARE_THROW_SCOPE(vm); + return throwVMTypeError(lexicalGlobalObject, throwScope); + } + + JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); + +#ifdef BUN_DEBUG + /** View the file name of the JS file that called this function + * from a debugger */ + SourceOrigin sourceOrigin = callFrame->callerSourceOrigin(vm); + const char* fileName = sourceOrigin.string().utf8().data(); + static const char* lastFileName = nullptr; + if (lastFileName != fileName) { + lastFileName = fileName; + } +#endif + + return ServerWebSocketPrototype__ping(thisObject->wrapped(), lexicalGlobalObject, callFrame); +} + +JSC_DEFINE_HOST_FUNCTION(ServerWebSocketPrototype__pongCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = lexicalGlobalObject->vm(); + + JSServerWebSocket* thisObject = jsDynamicCast<JSServerWebSocket*>(callFrame->thisValue()); + + if (UNLIKELY(!thisObject)) { + auto throwScope = DECLARE_THROW_SCOPE(vm); + return throwVMTypeError(lexicalGlobalObject, throwScope); + } + + JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); + +#ifdef BUN_DEBUG + /** View the file name of the JS file that called this function + * from a debugger */ + SourceOrigin sourceOrigin = callFrame->callerSourceOrigin(vm); + const char* fileName = sourceOrigin.string().utf8().data(); + static const char* lastFileName = nullptr; + if (lastFileName != fileName) { + lastFileName = fileName; + } +#endif + + return ServerWebSocketPrototype__pong(thisObject->wrapped(), lexicalGlobalObject, callFrame); +} + JSC_DEFINE_HOST_FUNCTION(ServerWebSocketPrototype__publishCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) { auto& vm = lexicalGlobalObject->vm(); @@ -15129,6 +15195,33 @@ JSC_DEFINE_HOST_FUNCTION(ServerWebSocketPrototype__subscribeCallback, (JSGlobalO return ServerWebSocketPrototype__subscribe(thisObject->wrapped(), lexicalGlobalObject, callFrame); } +JSC_DEFINE_HOST_FUNCTION(ServerWebSocketPrototype__terminateCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = lexicalGlobalObject->vm(); + + JSServerWebSocket* thisObject = jsDynamicCast<JSServerWebSocket*>(callFrame->thisValue()); + + if (UNLIKELY(!thisObject)) { + auto throwScope = DECLARE_THROW_SCOPE(vm); + return throwVMTypeError(lexicalGlobalObject, throwScope); + } + + JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); + +#ifdef BUN_DEBUG + /** View the file name of the JS file that called this function + * from a debugger */ + SourceOrigin sourceOrigin = callFrame->callerSourceOrigin(vm); + const char* fileName = sourceOrigin.string().utf8().data(); + static const char* lastFileName = nullptr; + if (lastFileName != fileName) { + lastFileName = fileName; + } +#endif + + return ServerWebSocketPrototype__terminate(thisObject->wrapped(), lexicalGlobalObject, callFrame); +} + JSC_DEFINE_HOST_FUNCTION(ServerWebSocketPrototype__unsubscribeCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) { auto& vm = lexicalGlobalObject->vm(); diff --git a/src/bun.js/bindings/generated_classes.zig b/src/bun.js/bindings/generated_classes.zig index 171bba792..5f83630c5 100644 --- a/src/bun.js/bindings/generated_classes.zig +++ b/src/bun.js/bindings/generated_classes.zig @@ -3834,6 +3834,10 @@ pub const JSServerWebSocket = struct { @compileLog("Expected ServerWebSocket.getBufferedAmount to be a callback but received " ++ @typeName(@TypeOf(ServerWebSocket.getBufferedAmount))); if (@TypeOf(ServerWebSocket.isSubscribed) != CallbackType) @compileLog("Expected ServerWebSocket.isSubscribed to be a callback but received " ++ @typeName(@TypeOf(ServerWebSocket.isSubscribed))); + if (@TypeOf(ServerWebSocket.ping) != CallbackType) + @compileLog("Expected ServerWebSocket.ping to be a callback but received " ++ @typeName(@TypeOf(ServerWebSocket.ping))); + if (@TypeOf(ServerWebSocket.pong) != CallbackType) + @compileLog("Expected ServerWebSocket.pong to be a callback but received " ++ @typeName(@TypeOf(ServerWebSocket.pong))); if (@TypeOf(ServerWebSocket.publish) != CallbackType) @compileLog("Expected ServerWebSocket.publish to be a callback but received " ++ @typeName(@TypeOf(ServerWebSocket.publish))); if (@TypeOf(ServerWebSocket.publishBinaryWithoutTypeChecks) != fn (*ServerWebSocket, *JSC.JSGlobalObject, *JSC.JSString, *JSC.JSUint8Array) callconv(.C) JSC.JSValue) @@ -3862,6 +3866,8 @@ pub const JSServerWebSocket = struct { @compileLog("Expected ServerWebSocket.sendText to be a callback but received " ++ @typeName(@TypeOf(ServerWebSocket.sendText))); if (@TypeOf(ServerWebSocket.subscribe) != CallbackType) @compileLog("Expected ServerWebSocket.subscribe to be a callback but received " ++ @typeName(@TypeOf(ServerWebSocket.subscribe))); + if (@TypeOf(ServerWebSocket.terminate) != CallbackType) + @compileLog("Expected ServerWebSocket.terminate to be a callback but received " ++ @typeName(@TypeOf(ServerWebSocket.terminate))); if (@TypeOf(ServerWebSocket.unsubscribe) != CallbackType) @compileLog("Expected ServerWebSocket.unsubscribe to be a callback but received " ++ @typeName(@TypeOf(ServerWebSocket.unsubscribe))); if (!JSC.is_bindgen) { @@ -3875,6 +3881,8 @@ pub const JSServerWebSocket = struct { @export(ServerWebSocket.getReadyState, .{ .name = "ServerWebSocketPrototype__getReadyState" }); @export(ServerWebSocket.getRemoteAddress, .{ .name = "ServerWebSocketPrototype__getRemoteAddress" }); @export(ServerWebSocket.isSubscribed, .{ .name = "ServerWebSocketPrototype__isSubscribed" }); + @export(ServerWebSocket.ping, .{ .name = "ServerWebSocketPrototype__ping" }); + @export(ServerWebSocket.pong, .{ .name = "ServerWebSocketPrototype__pong" }); @export(ServerWebSocket.publish, .{ .name = "ServerWebSocketPrototype__publish" }); @export(ServerWebSocket.publishBinary, .{ .name = "ServerWebSocketPrototype__publishBinary" }); @export(ServerWebSocket.publishBinaryWithoutTypeChecks, .{ .name = "ServerWebSocketPrototype__publishBinaryWithoutTypeChecks" }); @@ -3888,6 +3896,7 @@ pub const JSServerWebSocket = struct { @export(ServerWebSocket.setBinaryType, .{ .name = "ServerWebSocketPrototype__setBinaryType" }); @export(ServerWebSocket.setData, .{ .name = "ServerWebSocketPrototype__setData" }); @export(ServerWebSocket.subscribe, .{ .name = "ServerWebSocketPrototype__subscribe" }); + @export(ServerWebSocket.terminate, .{ .name = "ServerWebSocketPrototype__terminate" }); @export(ServerWebSocket.unsubscribe, .{ .name = "ServerWebSocketPrototype__unsubscribe" }); } } diff --git a/src/bun.js/bindings/headers.h b/src/bun.js/bindings/headers.h index f507121f8..1a6a40c9f 100644 --- a/src/bun.js/bindings/headers.h +++ b/src/bun.js/bindings/headers.h @@ -32,113 +32,113 @@ typedef void* JSClassRef; #include "JavaScriptCore/JSClassRef.h" #endif #include "headers-handwritten.h" - typedef struct bJSC__ThrowScope { unsigned char bytes[8]; } bJSC__ThrowScope; - typedef char* bJSC__ThrowScope_buf; - typedef struct bJSC__Exception { unsigned char bytes[40]; } bJSC__Exception; - typedef char* bJSC__Exception_buf; - typedef struct bJSC__VM { unsigned char bytes[52176]; } bJSC__VM; - typedef char* bJSC__VM_buf; - typedef struct bJSC__JSString { unsigned char bytes[16]; } bJSC__JSString; - typedef char* bJSC__JSString_buf; - typedef struct bJSC__JSGlobalObject { unsigned char bytes[3128]; } bJSC__JSGlobalObject; - typedef char* bJSC__JSGlobalObject_buf; - typedef struct bJSC__JSCell { unsigned char bytes[8]; } bJSC__JSCell; - typedef char* bJSC__JSCell_buf; - typedef struct bJSC__JSInternalPromise { unsigned char bytes[32]; } bJSC__JSInternalPromise; - typedef char* bJSC__JSInternalPromise_buf; typedef struct bJSC__JSPromise { unsigned char bytes[32]; } bJSC__JSPromise; typedef char* bJSC__JSPromise_buf; + typedef struct bJSC__JSCell { unsigned char bytes[8]; } bJSC__JSCell; + typedef char* bJSC__JSCell_buf; + typedef struct bJSC__Exception { unsigned char bytes[40]; } bJSC__Exception; + typedef char* bJSC__Exception_buf; typedef struct bJSC__JSObject { unsigned char bytes[16]; } bJSC__JSObject; typedef char* bJSC__JSObject_buf; + typedef struct bJSC__ThrowScope { unsigned char bytes[8]; } bJSC__ThrowScope; + typedef char* bJSC__ThrowScope_buf; typedef struct bJSC__CatchScope { unsigned char bytes[8]; } bJSC__CatchScope; typedef char* bJSC__CatchScope_buf; + typedef struct bJSC__JSString { unsigned char bytes[16]; } bJSC__JSString; + typedef char* bJSC__JSString_buf; + typedef struct bJSC__JSInternalPromise { unsigned char bytes[32]; } bJSC__JSInternalPromise; + typedef char* bJSC__JSInternalPromise_buf; + typedef struct bJSC__JSGlobalObject { unsigned char bytes[3128]; } bJSC__JSGlobalObject; + typedef char* bJSC__JSGlobalObject_buf; + typedef struct bJSC__VM { unsigned char bytes[52176]; } bJSC__VM; + typedef char* bJSC__VM_buf; #ifndef __cplusplus - typedef bJSC__CatchScope JSC__CatchScope; // JSC::CatchScope - typedef ErrorableResolvedSource ErrorableResolvedSource; + typedef Bun__ArrayBuffer Bun__ArrayBuffer; + typedef bJSC__JSString JSC__JSString; // JSC::JSString typedef BunString BunString; - typedef ErrorableString ErrorableString; - typedef bJSC__ThrowScope JSC__ThrowScope; // JSC::ThrowScope - typedef bJSC__JSObject JSC__JSObject; // JSC::JSObject - typedef WebSocketClient WebSocketClient; - typedef struct WebCore__AbortSignal WebCore__AbortSignal; // WebCore::AbortSignal - typedef struct JSC__JSMap JSC__JSMap; // JSC::JSMap - typedef WebSocketHTTPSClient WebSocketHTTPSClient; - typedef JSClassRef JSClassRef; + typedef int64_t JSC__JSValue; + typedef ZigString ZigString; + typedef struct WebCore__DOMFormData WebCore__DOMFormData; // WebCore::DOMFormData + typedef struct WebCore__DOMURL WebCore__DOMURL; // WebCore::DOMURL + typedef struct WebCore__FetchHeaders WebCore__FetchHeaders; // WebCore::FetchHeaders + typedef ErrorableResolvedSource ErrorableResolvedSource; + typedef bJSC__JSPromise JSC__JSPromise; // JSC::JSPromise typedef bJSC__VM JSC__VM; // JSC::VM - typedef Bun__ArrayBuffer Bun__ArrayBuffer; - typedef Uint8Array_alias Uint8Array_alias; - typedef WebSocketClientTLS WebSocketClientTLS; - typedef bJSC__JSGlobalObject JSC__JSGlobalObject; // JSC::JSGlobalObject + typedef bJSC__CatchScope JSC__CatchScope; // JSC::CatchScope typedef ZigException ZigException; - typedef bJSC__JSPromise JSC__JSPromise; // JSC::JSPromise + typedef struct JSC__CallFrame JSC__CallFrame; // JSC::CallFrame + typedef bJSC__ThrowScope JSC__ThrowScope; // JSC::ThrowScope + typedef bJSC__Exception JSC__Exception; // JSC::Exception typedef WebSocketHTTPClient WebSocketHTTPClient; + typedef WebSocketClient WebSocketClient; + typedef WebSocketClientTLS WebSocketClientTLS; + typedef ErrorableString ErrorableString; + typedef bJSC__JSObject JSC__JSObject; // JSC::JSObject + typedef struct JSC__JSMap JSC__JSMap; // JSC::JSMap typedef SystemError SystemError; + typedef Uint8Array_alias Uint8Array_alias; typedef bJSC__JSCell JSC__JSCell; // JSC::JSCell - typedef ZigString ZigString; - typedef struct WebCore__DOMURL WebCore__DOMURL; // WebCore::DOMURL - typedef int64_t JSC__JSValue; + typedef bJSC__JSGlobalObject JSC__JSGlobalObject; // JSC::JSGlobalObject + typedef struct WebCore__AbortSignal WebCore__AbortSignal; // WebCore::AbortSignal + typedef JSClassRef JSClassRef; typedef bJSC__JSInternalPromise JSC__JSInternalPromise; // JSC::JSInternalPromise - typedef bJSC__Exception JSC__Exception; // JSC::Exception - typedef bJSC__JSString JSC__JSString; // JSC::JSString - typedef struct WebCore__DOMFormData WebCore__DOMFormData; // WebCore::DOMFormData - typedef struct JSC__CallFrame JSC__CallFrame; // JSC::CallFrame - typedef struct WebCore__FetchHeaders WebCore__FetchHeaders; // WebCore::FetchHeaders + typedef WebSocketHTTPSClient WebSocketHTTPSClient; #endif #ifdef __cplusplus namespace JSC { - class JSMap; - class JSCell; - class JSObject; class JSGlobalObject; - class JSPromise; class Exception; - class JSString; + class JSObject; class JSInternalPromise; + class JSString; + class JSCell; + class JSMap; + class JSPromise; class CatchScope; class VM; - class CallFrame; class ThrowScope; + class CallFrame; } namespace WebCore { + class FetchHeaders; class DOMFormData; - class DOMURL; class AbortSignal; - class FetchHeaders; + class DOMURL; } - typedef ErrorableResolvedSource ErrorableResolvedSource; - typedef BunString BunString; - typedef ErrorableString ErrorableString; - typedef WebSocketClient WebSocketClient; - typedef WebSocketHTTPSClient WebSocketHTTPSClient; - typedef JSClassRef JSClassRef; typedef Bun__ArrayBuffer Bun__ArrayBuffer; - typedef Uint8Array_alias Uint8Array_alias; - typedef WebSocketClientTLS WebSocketClientTLS; + typedef BunString BunString; + typedef int64_t JSC__JSValue; + typedef ZigString ZigString; + typedef ErrorableResolvedSource ErrorableResolvedSource; typedef ZigException ZigException; typedef WebSocketHTTPClient WebSocketHTTPClient; + typedef WebSocketClient WebSocketClient; + typedef WebSocketClientTLS WebSocketClientTLS; + typedef ErrorableString ErrorableString; typedef SystemError SystemError; - typedef ZigString ZigString; - typedef int64_t JSC__JSValue; - using JSC__JSMap = JSC::JSMap; - using JSC__JSCell = JSC::JSCell; - using JSC__JSObject = JSC::JSObject; + typedef Uint8Array_alias Uint8Array_alias; + typedef JSClassRef JSClassRef; + typedef WebSocketHTTPSClient WebSocketHTTPSClient; using JSC__JSGlobalObject = JSC::JSGlobalObject; - using JSC__JSPromise = JSC::JSPromise; using JSC__Exception = JSC::Exception; - using JSC__JSString = JSC::JSString; + using JSC__JSObject = JSC::JSObject; using JSC__JSInternalPromise = JSC::JSInternalPromise; + using JSC__JSString = JSC::JSString; + using JSC__JSCell = JSC::JSCell; + using JSC__JSMap = JSC::JSMap; + using JSC__JSPromise = JSC::JSPromise; using JSC__CatchScope = JSC::CatchScope; using JSC__VM = JSC::VM; - using JSC__CallFrame = JSC::CallFrame; using JSC__ThrowScope = JSC::ThrowScope; + using JSC__CallFrame = JSC::CallFrame; + using WebCore__FetchHeaders = WebCore::FetchHeaders; using WebCore__DOMFormData = WebCore::DOMFormData; - using WebCore__DOMURL = WebCore::DOMURL; using WebCore__AbortSignal = WebCore::AbortSignal; - using WebCore__FetchHeaders = WebCore::FetchHeaders; + using WebCore__DOMURL = WebCore::DOMURL; #endif @@ -734,23 +734,25 @@ ZIG_DECL void Bun__WebSocketHTTPSClient__register(JSC__JSGlobalObject* arg0, voi #ifdef __cplusplus +ZIG_DECL void Bun__WebSocketClient__cancel(WebSocketClient* arg0); ZIG_DECL void Bun__WebSocketClient__close(WebSocketClient* arg0, uint16_t arg1, const ZigString* arg2); ZIG_DECL void Bun__WebSocketClient__finalize(WebSocketClient* arg0); ZIG_DECL void* Bun__WebSocketClient__init(CppWebSocket* arg0, void* arg1, void* arg2, JSC__JSGlobalObject* arg3, unsigned char* arg4, size_t arg5); ZIG_DECL void Bun__WebSocketClient__register(JSC__JSGlobalObject* arg0, void* arg1, void* arg2); -ZIG_DECL void Bun__WebSocketClient__writeBinaryData(WebSocketClient* arg0, const unsigned char* arg1, size_t arg2); -ZIG_DECL void Bun__WebSocketClient__writeString(WebSocketClient* arg0, const ZigString* arg1); +ZIG_DECL void Bun__WebSocketClient__writeBinaryData(WebSocketClient* arg0, const unsigned char* arg1, size_t arg2, unsigned char arg3); +ZIG_DECL void Bun__WebSocketClient__writeString(WebSocketClient* arg0, const ZigString* arg1, unsigned char arg2); #endif #ifdef __cplusplus +ZIG_DECL void Bun__WebSocketClientTLS__cancel(WebSocketClientTLS* arg0); ZIG_DECL void Bun__WebSocketClientTLS__close(WebSocketClientTLS* arg0, uint16_t arg1, const ZigString* arg2); ZIG_DECL void Bun__WebSocketClientTLS__finalize(WebSocketClientTLS* arg0); ZIG_DECL void* Bun__WebSocketClientTLS__init(CppWebSocket* arg0, void* arg1, void* arg2, JSC__JSGlobalObject* arg3, unsigned char* arg4, size_t arg5); ZIG_DECL void Bun__WebSocketClientTLS__register(JSC__JSGlobalObject* arg0, void* arg1, void* arg2); -ZIG_DECL void Bun__WebSocketClientTLS__writeBinaryData(WebSocketClientTLS* arg0, const unsigned char* arg1, size_t arg2); -ZIG_DECL void Bun__WebSocketClientTLS__writeString(WebSocketClientTLS* arg0, const ZigString* arg1); +ZIG_DECL void Bun__WebSocketClientTLS__writeBinaryData(WebSocketClientTLS* arg0, const unsigned char* arg1, size_t arg2, unsigned char arg3); +ZIG_DECL void Bun__WebSocketClientTLS__writeString(WebSocketClientTLS* arg0, const ZigString* arg1, unsigned char arg2); #endif diff --git a/src/bun.js/bindings/headers.zig b/src/bun.js/bindings/headers.zig index 666369b21..be2e4a626 100644 --- a/src/bun.js/bindings/headers.zig +++ b/src/bun.js/bindings/headers.zig @@ -72,15 +72,15 @@ pub const WebSocketHTTPSClient = bindings.WebSocketHTTPSClient; pub const WebSocketClient = bindings.WebSocketClient; pub const WebSocketClientTLS = bindings.WebSocketClientTLS; pub const BunString = @import("root").bun.String; +pub const JSC__JSString = bJSC__JSString; +pub const JSC__JSPromise = bJSC__JSPromise; +pub const JSC__VM = bJSC__VM; pub const JSC__ThrowScope = bJSC__ThrowScope; +pub const JSC__Exception = bJSC__Exception; pub const JSC__JSObject = bJSC__JSObject; -pub const JSC__VM = bJSC__VM; -pub const JSC__JSGlobalObject = bJSC__JSGlobalObject; -pub const JSC__JSPromise = bJSC__JSPromise; pub const JSC__JSCell = bJSC__JSCell; +pub const JSC__JSGlobalObject = bJSC__JSGlobalObject; pub const JSC__JSInternalPromise = bJSC__JSInternalPromise; -pub const JSC__Exception = bJSC__Exception; -pub const JSC__JSString = bJSC__JSString; pub extern fn JSC__JSObject__create(arg0: *bindings.JSGlobalObject, arg1: usize, arg2: ?*anyopaque, ArgFn3: ?*const fn (?*anyopaque, [*c]bindings.JSObject, *bindings.JSGlobalObject) callconv(.C) void) JSC__JSValue; pub extern fn JSC__JSObject__getArrayLength(arg0: [*c]bindings.JSObject) usize; pub extern fn JSC__JSObject__getDirect(arg0: [*c]bindings.JSObject, arg1: *bindings.JSGlobalObject, arg2: [*c]const ZigString) JSC__JSValue; diff --git a/src/bun.js/bindings/webcore/JSWebSocket.cpp b/src/bun.js/bindings/webcore/JSWebSocket.cpp index de5c644e7..7b099544b 100644 --- a/src/bun.js/bindings/webcore/JSWebSocket.cpp +++ b/src/bun.js/bindings/webcore/JSWebSocket.cpp @@ -71,6 +71,9 @@ using namespace JSC; static JSC_DECLARE_HOST_FUNCTION(jsWebSocketPrototypeFunction_send); static JSC_DECLARE_HOST_FUNCTION(jsWebSocketPrototypeFunction_close); +static JSC_DECLARE_HOST_FUNCTION(jsWebSocketPrototypeFunction_ping); +static JSC_DECLARE_HOST_FUNCTION(jsWebSocketPrototypeFunction_pong); +static JSC_DECLARE_HOST_FUNCTION(jsWebSocketPrototypeFunction_terminate); // Attributes @@ -293,7 +296,7 @@ template<> void JSWebSocketDOMConstructor::initializeProperties(VM& vm, JSDOMGlo static const HashTableValue JSWebSocketPrototypeTableValues[] = { { "constructor"_s, static_cast<unsigned>(JSC::PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::GetterSetterType, jsWebSocketConstructor, 0 } }, - { "URL"_s, static_cast<unsigned>(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute), NoIntrinsic, { HashTableValue::GetterSetterType, jsWebSocket_URL, 0 } }, + { "URL"_s, static_cast<unsigned>(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute | JSC::PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::GetterSetterType, jsWebSocket_URL, 0 } }, { "url"_s, static_cast<unsigned>(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute), NoIntrinsic, { HashTableValue::GetterSetterType, jsWebSocket_url, 0 } }, { "readyState"_s, static_cast<unsigned>(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute), NoIntrinsic, { HashTableValue::GetterSetterType, jsWebSocket_readyState, 0 } }, { "bufferedAmount"_s, static_cast<unsigned>(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute), NoIntrinsic, { HashTableValue::GetterSetterType, jsWebSocket_bufferedAmount, 0 } }, @@ -306,6 +309,9 @@ static const HashTableValue JSWebSocketPrototypeTableValues[] = { { "binaryType"_s, static_cast<unsigned>(JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute), NoIntrinsic, { HashTableValue::GetterSetterType, jsWebSocket_binaryType, setJSWebSocket_binaryType } }, { "send"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsWebSocketPrototypeFunction_send, 1 } }, { "close"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsWebSocketPrototypeFunction_close, 0 } }, + { "ping"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsWebSocketPrototypeFunction_ping, 1 } }, + { "pong"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsWebSocketPrototypeFunction_pong, 1 } }, + { "terminate"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsWebSocketPrototypeFunction_terminate, 0 } }, { "CONNECTING"_s, JSC::PropertyAttribute::DontDelete | JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::ConstantInteger, NoIntrinsic, { HashTableValue::ConstantType, 0 } }, { "OPEN"_s, JSC::PropertyAttribute::DontDelete | JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::ConstantInteger, NoIntrinsic, { HashTableValue::ConstantType, 1 } }, { "CLOSING"_s, JSC::PropertyAttribute::DontDelete | JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::ConstantInteger, NoIntrinsic, { HashTableValue::ConstantType, 2 } }, @@ -672,6 +678,199 @@ JSC_DEFINE_HOST_FUNCTION(jsWebSocketPrototypeFunction_close, (JSGlobalObject * l return IDLOperation<JSWebSocket>::call<jsWebSocketPrototypeFunction_closeBody>(*lexicalGlobalObject, *callFrame, "close"); } +static inline JSC::EncodedJSValue jsWebSocketPrototypeFunction_ping1Body(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame, typename IDLOperation<JSWebSocket>::ClassParameter castedThis) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + UNUSED_PARAM(throwScope); + UNUSED_PARAM(callFrame); + auto& impl = castedThis->wrapped(); + RELEASE_AND_RETURN(throwScope, JSValue::encode(toJS<IDLUndefined>(*lexicalGlobalObject, throwScope, [&]() -> decltype(auto) { return impl.ping(); }))); +} + +static inline JSC::EncodedJSValue jsWebSocketPrototypeFunction_ping2Body(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame, typename IDLOperation<JSWebSocket>::ClassParameter castedThis) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + UNUSED_PARAM(throwScope); + UNUSED_PARAM(callFrame); + auto& impl = castedThis->wrapped(); + EnsureStillAliveScope argument0 = callFrame->uncheckedArgument(0); + auto data = convert<IDLArrayBuffer>(*lexicalGlobalObject, argument0.value(), [](JSC::JSGlobalObject& lexicalGlobalObject, JSC::ThrowScope& scope) { throwArgumentTypeError(lexicalGlobalObject, scope, 0, "data", "WebSocket", "ping", "ArrayBuffer"); }); + RETURN_IF_EXCEPTION(throwScope, encodedJSValue()); + RELEASE_AND_RETURN(throwScope, JSValue::encode(toJS<IDLUndefined>(*lexicalGlobalObject, throwScope, [&]() -> decltype(auto) { return impl.ping(*data); }))); +} + +static inline JSC::EncodedJSValue jsWebSocketPrototypeFunction_ping3Body(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame, typename IDLOperation<JSWebSocket>::ClassParameter castedThis) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + UNUSED_PARAM(throwScope); + UNUSED_PARAM(callFrame); + auto& impl = castedThis->wrapped(); + EnsureStillAliveScope argument0 = callFrame->uncheckedArgument(0); + auto data = convert<IDLArrayBufferView>(*lexicalGlobalObject, argument0.value(), [](JSC::JSGlobalObject& lexicalGlobalObject, JSC::ThrowScope& scope) { throwArgumentTypeError(lexicalGlobalObject, scope, 0, "data", "WebSocket", "ping", "ArrayBufferView"); }); + RETURN_IF_EXCEPTION(throwScope, encodedJSValue()); + RELEASE_AND_RETURN(throwScope, JSValue::encode(toJS<IDLUndefined>(*lexicalGlobalObject, throwScope, [&]() -> decltype(auto) { return impl.ping(data.releaseNonNull()); }))); +} + +// static inline JSC::EncodedJSValue jsWebSocketPrototypeFunction_ping4Body(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame, typename IDLOperation<JSWebSocket>::ClassParameter castedThis) +// { +// auto& vm = JSC::getVM(lexicalGlobalObject); +// auto throwScope = DECLARE_THROW_SCOPE(vm); +// UNUSED_PARAM(throwScope); +// UNUSED_PARAM(callFrame); +// auto& impl = castedThis->wrapped(); +// EnsureStillAliveScope argument0 = callFrame->uncheckedArgument(0); +// auto data = convert<IDLInterface<Blob>>(*lexicalGlobalObject, argument0.value(), [](JSC::JSGlobalObject& lexicalGlobalObject, JSC::ThrowScope& scope) { throwArgumentTypeError(lexicalGlobalObject, scope, 0, "data", "WebSocket", "ping", "Blob"); }); +// RETURN_IF_EXCEPTION(throwScope, encodedJSValue()); +// RELEASE_AND_RETURN(throwScope, JSValue::encode(toJS<IDLUndefined>(*lexicalGlobalObject, throwScope, [&]() -> decltype(auto) { return impl.ping(*data); }))); +// } + +static inline JSC::EncodedJSValue jsWebSocketPrototypeFunction_ping5Body(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame, typename IDLOperation<JSWebSocket>::ClassParameter castedThis) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + UNUSED_PARAM(throwScope); + UNUSED_PARAM(callFrame); + auto& impl = castedThis->wrapped(); + EnsureStillAliveScope argument0 = callFrame->uncheckedArgument(0); + auto data = convert<IDLUSVString>(*lexicalGlobalObject, argument0.value()); + RETURN_IF_EXCEPTION(throwScope, encodedJSValue()); + RELEASE_AND_RETURN(throwScope, JSValue::encode(toJS<IDLUndefined>(*lexicalGlobalObject, throwScope, [&]() -> decltype(auto) { return impl.ping(WTFMove(data)); }))); +} + +static inline JSC::EncodedJSValue jsWebSocketPrototypeFunction_pingOverloadDispatcher(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame, typename IDLOperation<JSWebSocket>::ClassParameter castedThis) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + UNUSED_PARAM(throwScope); + UNUSED_PARAM(callFrame); + size_t argsCount = std::min<size_t>(1, callFrame->argumentCount()); + if (argsCount == 0) { + RELEASE_AND_RETURN(throwScope, (jsWebSocketPrototypeFunction_ping1Body(lexicalGlobalObject, callFrame, castedThis))); + } else if (argsCount == 1) { + JSValue distinguishingArg = callFrame->uncheckedArgument(0); + if (distinguishingArg.isObject() && asObject(distinguishingArg)->inherits<JSArrayBuffer>()) + RELEASE_AND_RETURN(throwScope, (jsWebSocketPrototypeFunction_ping2Body(lexicalGlobalObject, callFrame, castedThis))); + if (distinguishingArg.isObject() && asObject(distinguishingArg)->inherits<JSArrayBufferView>()) + RELEASE_AND_RETURN(throwScope, (jsWebSocketPrototypeFunction_ping3Body(lexicalGlobalObject, callFrame, castedThis))); + // if (distinguishingArg.isObject() && asObject(distinguishingArg)->inherits<JSBlob>()) + // RELEASE_AND_RETURN(throwScope, (jsWebSocketPrototypeFunction_ping4Body(lexicalGlobalObject, callFrame, castedThis))); + RELEASE_AND_RETURN(throwScope, (jsWebSocketPrototypeFunction_ping5Body(lexicalGlobalObject, callFrame, castedThis))); + } + return throwVMTypeError(lexicalGlobalObject, throwScope); +} + +JSC_DEFINE_HOST_FUNCTION(jsWebSocketPrototypeFunction_ping, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + return IDLOperation<JSWebSocket>::call<jsWebSocketPrototypeFunction_pingOverloadDispatcher>(*lexicalGlobalObject, *callFrame, "ping"); +} + +static inline JSC::EncodedJSValue jsWebSocketPrototypeFunction_pong1Body(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame, typename IDLOperation<JSWebSocket>::ClassParameter castedThis) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + UNUSED_PARAM(throwScope); + UNUSED_PARAM(callFrame); + auto& impl = castedThis->wrapped(); + RELEASE_AND_RETURN(throwScope, JSValue::encode(toJS<IDLUndefined>(*lexicalGlobalObject, throwScope, [&]() -> decltype(auto) { return impl.pong(); }))); +} + +static inline JSC::EncodedJSValue jsWebSocketPrototypeFunction_pong2Body(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame, typename IDLOperation<JSWebSocket>::ClassParameter castedThis) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + UNUSED_PARAM(throwScope); + UNUSED_PARAM(callFrame); + auto& impl = castedThis->wrapped(); + EnsureStillAliveScope argument0 = callFrame->uncheckedArgument(0); + auto data = convert<IDLArrayBuffer>(*lexicalGlobalObject, argument0.value(), [](JSC::JSGlobalObject& lexicalGlobalObject, JSC::ThrowScope& scope) { throwArgumentTypeError(lexicalGlobalObject, scope, 0, "data", "WebSocket", "pong", "ArrayBuffer"); }); + RETURN_IF_EXCEPTION(throwScope, encodedJSValue()); + RELEASE_AND_RETURN(throwScope, JSValue::encode(toJS<IDLUndefined>(*lexicalGlobalObject, throwScope, [&]() -> decltype(auto) { return impl.pong(*data); }))); +} + +static inline JSC::EncodedJSValue jsWebSocketPrototypeFunction_pong3Body(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame, typename IDLOperation<JSWebSocket>::ClassParameter castedThis) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + UNUSED_PARAM(throwScope); + UNUSED_PARAM(callFrame); + auto& impl = castedThis->wrapped(); + EnsureStillAliveScope argument0 = callFrame->uncheckedArgument(0); + auto data = convert<IDLArrayBufferView>(*lexicalGlobalObject, argument0.value(), [](JSC::JSGlobalObject& lexicalGlobalObject, JSC::ThrowScope& scope) { throwArgumentTypeError(lexicalGlobalObject, scope, 0, "data", "WebSocket", "pong", "ArrayBufferView"); }); + RETURN_IF_EXCEPTION(throwScope, encodedJSValue()); + RELEASE_AND_RETURN(throwScope, JSValue::encode(toJS<IDLUndefined>(*lexicalGlobalObject, throwScope, [&]() -> decltype(auto) { return impl.pong(data.releaseNonNull()); }))); +} + +// static inline JSC::EncodedJSValue jsWebSocketPrototypeFunction_pong4Body(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame, typename IDLOperation<JSWebSocket>::ClassParameter castedThis) +// { +// auto& vm = JSC::getVM(lexicalGlobalObject); +// auto throwScope = DECLARE_THROW_SCOPE(vm); +// UNUSED_PARAM(throwScope); +// UNUSED_PARAM(callFrame); +// auto& impl = castedThis->wrapped(); +// EnsureStillAliveScope argument0 = callFrame->uncheckedArgument(0); +// auto data = convert<IDLInterface<Blob>>(*lexicalGlobalObject, argument0.value(), [](JSC::JSGlobalObject& lexicalGlobalObject, JSC::ThrowScope& scope) { throwArgumentTypeError(lexicalGlobalObject, scope, 0, "data", "WebSocket", "pong", "Blob"); }); +// RETURN_IF_EXCEPTION(throwScope, encodedJSValue()); +// RELEASE_AND_RETURN(throwScope, JSValue::encode(toJS<IDLUndefined>(*lexicalGlobalObject, throwScope, [&]() -> decltype(auto) { return impl.pong(*data); }))); +// } + +static inline JSC::EncodedJSValue jsWebSocketPrototypeFunction_pong5Body(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame, typename IDLOperation<JSWebSocket>::ClassParameter castedThis) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + UNUSED_PARAM(throwScope); + UNUSED_PARAM(callFrame); + auto& impl = castedThis->wrapped(); + EnsureStillAliveScope argument0 = callFrame->uncheckedArgument(0); + auto data = convert<IDLUSVString>(*lexicalGlobalObject, argument0.value()); + RETURN_IF_EXCEPTION(throwScope, encodedJSValue()); + RELEASE_AND_RETURN(throwScope, JSValue::encode(toJS<IDLUndefined>(*lexicalGlobalObject, throwScope, [&]() -> decltype(auto) { return impl.pong(WTFMove(data)); }))); +} + +static inline JSC::EncodedJSValue jsWebSocketPrototypeFunction_pongOverloadDispatcher(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame, typename IDLOperation<JSWebSocket>::ClassParameter castedThis) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + UNUSED_PARAM(throwScope); + UNUSED_PARAM(callFrame); + size_t argsCount = std::min<size_t>(1, callFrame->argumentCount()); + if (argsCount == 0) { + RELEASE_AND_RETURN(throwScope, (jsWebSocketPrototypeFunction_pong1Body(lexicalGlobalObject, callFrame, castedThis))); + } else if (argsCount == 1) { + JSValue distinguishingArg = callFrame->uncheckedArgument(0); + if (distinguishingArg.isObject() && asObject(distinguishingArg)->inherits<JSArrayBuffer>()) + RELEASE_AND_RETURN(throwScope, (jsWebSocketPrototypeFunction_pong2Body(lexicalGlobalObject, callFrame, castedThis))); + if (distinguishingArg.isObject() && asObject(distinguishingArg)->inherits<JSArrayBufferView>()) + RELEASE_AND_RETURN(throwScope, (jsWebSocketPrototypeFunction_pong3Body(lexicalGlobalObject, callFrame, castedThis))); + // if (distinguishingArg.isObject() && asObject(distinguishingArg)->inherits<JSBlob>()) + // RELEASE_AND_RETURN(throwScope, (jsWebSocketPrototypeFunction_pong4Body(lexicalGlobalObject, callFrame, castedThis))); + RELEASE_AND_RETURN(throwScope, (jsWebSocketPrototypeFunction_pong5Body(lexicalGlobalObject, callFrame, castedThis))); + } + return throwVMTypeError(lexicalGlobalObject, throwScope); +} + +JSC_DEFINE_HOST_FUNCTION(jsWebSocketPrototypeFunction_pong, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + return IDLOperation<JSWebSocket>::call<jsWebSocketPrototypeFunction_pongOverloadDispatcher>(*lexicalGlobalObject, *callFrame, "pong"); +} + +static inline JSC::EncodedJSValue jsWebSocketPrototypeFunction_terminateBody(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame, typename IDLOperation<JSWebSocket>::ClassParameter castedThis) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + UNUSED_PARAM(throwScope); + UNUSED_PARAM(callFrame); + auto& impl = castedThis->wrapped(); + RELEASE_AND_RETURN(throwScope, JSValue::encode(toJS<IDLUndefined>(*lexicalGlobalObject, throwScope, [&]() -> decltype(auto) { return impl.terminate(); }))); +} + +JSC_DEFINE_HOST_FUNCTION(jsWebSocketPrototypeFunction_terminate, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + return IDLOperation<JSWebSocket>::call<jsWebSocketPrototypeFunction_terminateBody>(*lexicalGlobalObject, *callFrame, "terminate"); +} + JSC::GCClient::IsoSubspace* JSWebSocket::subspaceForImpl(JSC::VM& vm) { return WebCore::subspaceForImpl<JSWebSocket, UseCustomHeapCellType::No>( diff --git a/src/bun.js/bindings/webcore/WebSocket.cpp b/src/bun.js/bindings/webcore/WebSocket.cpp index 1d6392f44..c1a4054f5 100644 --- a/src/bun.js/bindings/webcore/WebSocket.cpp +++ b/src/bun.js/bindings/webcore/WebSocket.cpp @@ -458,8 +458,7 @@ ExceptionOr<void> WebSocket::send(const String& message) return {}; } - // 0-length is allowed - this->sendWebSocketString(message); + this->sendWebSocketString(message, Opcode::Text); return {}; } @@ -477,8 +476,8 @@ ExceptionOr<void> WebSocket::send(ArrayBuffer& binaryData) } char* data = static_cast<char*>(binaryData.data()); size_t length = binaryData.byteLength(); - // 0-length is allowed - this->sendWebSocketData(data, length); + this->sendWebSocketData(data, length, Opcode::Binary); + return {}; } @@ -498,8 +497,7 @@ ExceptionOr<void> WebSocket::send(ArrayBufferView& arrayBufferView) auto buffer = arrayBufferView.unsharedBuffer().get(); char* baseAddress = reinterpret_cast<char*>(buffer->data()) + arrayBufferView.byteOffset(); size_t length = arrayBufferView.byteLength(); - // 0-length is allowed - this->sendWebSocketData(baseAddress, length); + this->sendWebSocketData(baseAddress, length, Opcode::Binary); return {}; } @@ -521,17 +519,17 @@ ExceptionOr<void> WebSocket::send(ArrayBufferView& arrayBufferView) // return {}; // } -void WebSocket::sendWebSocketData(const char* baseAddress, size_t length) +void WebSocket::sendWebSocketData(const char* baseAddress, size_t length, const Opcode op) { switch (m_connectedWebSocketKind) { case ConnectedWebSocketKind::Client: { - Bun__WebSocketClient__writeBinaryData(this->m_connectedWebSocket.client, reinterpret_cast<const unsigned char*>(baseAddress), length); + Bun__WebSocketClient__writeBinaryData(this->m_connectedWebSocket.client, reinterpret_cast<const unsigned char*>(baseAddress), length, static_cast<uint8_t>(op)); // this->m_connectedWebSocket.client->send({ baseAddress, length }, opCode); // this->m_bufferedAmount = this->m_connectedWebSocket.client->getBufferedAmount(); break; } case ConnectedWebSocketKind::ClientSSL: { - Bun__WebSocketClientTLS__writeBinaryData(this->m_connectedWebSocket.clientSSL, reinterpret_cast<const unsigned char*>(baseAddress), length); + Bun__WebSocketClientTLS__writeBinaryData(this->m_connectedWebSocket.clientSSL, reinterpret_cast<const unsigned char*>(baseAddress), length, static_cast<uint8_t>(op)); break; } // case ConnectedWebSocketKind::Server: { @@ -550,19 +548,19 @@ void WebSocket::sendWebSocketData(const char* baseAddress, size_t length) } } -void WebSocket::sendWebSocketString(const String& message) +void WebSocket::sendWebSocketString(const String& message, const Opcode op) { switch (m_connectedWebSocketKind) { case ConnectedWebSocketKind::Client: { auto zigStr = Zig::toZigString(message); - Bun__WebSocketClient__writeString(this->m_connectedWebSocket.client, &zigStr); + Bun__WebSocketClient__writeString(this->m_connectedWebSocket.client, &zigStr, static_cast<uint8_t>(op)); // this->m_connectedWebSocket.client->send({ baseAddress, length }, opCode); // this->m_bufferedAmount = this->m_connectedWebSocket.client->getBufferedAmount(); break; } case ConnectedWebSocketKind::ClientSSL: { auto zigStr = Zig::toZigString(message); - Bun__WebSocketClientTLS__writeString(this->m_connectedWebSocket.clientSSL, &zigStr); + Bun__WebSocketClientTLS__writeString(this->m_connectedWebSocket.clientSSL, &zigStr, static_cast<uint8_t>(op)); break; } // case ConnectedWebSocketKind::Server: { @@ -586,8 +584,8 @@ void WebSocket::sendWebSocketString(const String& message) ExceptionOr<void> WebSocket::close(std::optional<unsigned short> optionalCode, const String& reason) { - int code = optionalCode ? optionalCode.value() : static_cast<int>(0); - if (code == 0) { + int code = optionalCode ? optionalCode.value() : static_cast<int>(1000); + if (code == 1000) { // LOG(Network, "WebSocket %p close() without code and reason", this); } else { // LOG(Network, "WebSocket %p close() code=%d reason='%s'", this, code, reason.utf8().data()); @@ -650,6 +648,211 @@ ExceptionOr<void> WebSocket::close(std::optional<unsigned short> optionalCode, c return {}; } +ExceptionOr<void> WebSocket::terminate() +{ + LOG(Network, "WebSocket %p terminate()", this); + + if (m_state == CLOSING || m_state == CLOSED) + return {}; + if (m_state == CONNECTING) { + m_state = CLOSING; + if (m_upgradeClient != nullptr) { + void* upgradeClient = m_upgradeClient; + m_upgradeClient = nullptr; + if (m_isSecure) { + Bun__WebSocketHTTPSClient__cancel(upgradeClient); + } else { + Bun__WebSocketHTTPClient__cancel(upgradeClient); + } + } + updateHasPendingActivity(); + return {}; + } + m_state = CLOSING; + switch (m_connectedWebSocketKind) { + case ConnectedWebSocketKind::Client: { + Bun__WebSocketClient__cancel(this->m_connectedWebSocket.client); + updateHasPendingActivity(); + break; + } + case ConnectedWebSocketKind::ClientSSL: { + Bun__WebSocketClientTLS__cancel(this->m_connectedWebSocket.clientSSL); + updateHasPendingActivity(); + break; + } + default: { + break; + } + } + this->m_connectedWebSocketKind = ConnectedWebSocketKind::None; + updateHasPendingActivity(); + return {}; +} + +ExceptionOr<void> WebSocket::ping() +{ + auto message = WTF::String::number(WTF::jsCurrentTime()); + LOG(Network, "WebSocket %p ping() Sending Timestamp '%s'", this, message.data()); + if (m_state == CONNECTING) + return Exception { InvalidStateError }; + + // No exception is raised if the connection was once established but has subsequently been closed. + if (m_state == CLOSING || m_state == CLOSED) { + size_t payloadSize = message.length(); + m_bufferedAmountAfterClose = saturateAdd(m_bufferedAmountAfterClose, payloadSize); + m_bufferedAmountAfterClose = saturateAdd(m_bufferedAmountAfterClose, getFramingOverhead(payloadSize)); + return {}; + } + + this->sendWebSocketString(message, Opcode::Ping); + + return {}; +} + +ExceptionOr<void> WebSocket::ping(const String& message) +{ + LOG(Network, "WebSocket %p ping() Sending String '%s'", this, message.utf8().data()); + if (m_state == CONNECTING) + return Exception { InvalidStateError }; + + // No exception is raised if the connection was once established but has subsequently been closed. + if (m_state == CLOSING || m_state == CLOSED) { + auto utf8 = message.utf8(StrictConversionReplacingUnpairedSurrogatesWithFFFD); + size_t payloadSize = utf8.length(); + m_bufferedAmountAfterClose = saturateAdd(m_bufferedAmountAfterClose, payloadSize); + m_bufferedAmountAfterClose = saturateAdd(m_bufferedAmountAfterClose, getFramingOverhead(payloadSize)); + return {}; + } + + this->sendWebSocketString(message, Opcode::Ping); + + return {}; +} + +ExceptionOr<void> WebSocket::ping(ArrayBuffer& binaryData) +{ + LOG(Network, "WebSocket %p ping() Sending ArrayBuffer %p", this, &binaryData); + if (m_state == CONNECTING) + return Exception { InvalidStateError }; + + if (m_state == CLOSING || m_state == CLOSED) { + unsigned payloadSize = binaryData.byteLength(); + m_bufferedAmountAfterClose = saturateAdd(m_bufferedAmountAfterClose, payloadSize); + m_bufferedAmountAfterClose = saturateAdd(m_bufferedAmountAfterClose, getFramingOverhead(payloadSize)); + return {}; + } + + char* data = static_cast<char*>(binaryData.data()); + size_t length = binaryData.byteLength(); + this->sendWebSocketData(data, length, Opcode::Ping); + + return {}; +} + +ExceptionOr<void> WebSocket::ping(ArrayBufferView& arrayBufferView) +{ + LOG(Network, "WebSocket %p ping() Sending ArrayBufferView %p", this, &arrayBufferView); + + if (m_state == CONNECTING) + return Exception { InvalidStateError }; + + if (m_state == CLOSING || m_state == CLOSED) { + unsigned payloadSize = arrayBufferView.byteLength(); + m_bufferedAmountAfterClose = saturateAdd(m_bufferedAmountAfterClose, payloadSize); + m_bufferedAmountAfterClose = saturateAdd(m_bufferedAmountAfterClose, getFramingOverhead(payloadSize)); + return {}; + } + + auto buffer = arrayBufferView.unsharedBuffer().get(); + char* baseAddress = reinterpret_cast<char*>(buffer->data()) + arrayBufferView.byteOffset(); + size_t length = arrayBufferView.byteLength(); + this->sendWebSocketData(baseAddress, length, Opcode::Ping); + + return {}; +} + +ExceptionOr<void> WebSocket::pong() +{ + auto message = WTF::String::number(WTF::jsCurrentTime()); + LOG(Network, "WebSocket %p pong() Sending Timestamp '%s'", this, message.data()); + if (m_state == CONNECTING) + return Exception { InvalidStateError }; + + // No exception is raised if the connection was once established but has subsequently been closed. + if (m_state == CLOSING || m_state == CLOSED) { + size_t payloadSize = message.length(); + m_bufferedAmountAfterClose = saturateAdd(m_bufferedAmountAfterClose, payloadSize); + m_bufferedAmountAfterClose = saturateAdd(m_bufferedAmountAfterClose, getFramingOverhead(payloadSize)); + return {}; + } + + this->sendWebSocketString(message, Opcode::Pong); + + return {}; +} + +ExceptionOr<void> WebSocket::pong(const String& message) +{ + LOG(Network, "WebSocket %p pong() Sending String '%s'", this, message.utf8().data()); + if (m_state == CONNECTING) + return Exception { InvalidStateError }; + + // No exception is raised if the connection was once established but has subsequently been closed. + if (m_state == CLOSING || m_state == CLOSED) { + auto utf8 = message.utf8(StrictConversionReplacingUnpairedSurrogatesWithFFFD); + size_t payloadSize = utf8.length(); + m_bufferedAmountAfterClose = saturateAdd(m_bufferedAmountAfterClose, payloadSize); + m_bufferedAmountAfterClose = saturateAdd(m_bufferedAmountAfterClose, getFramingOverhead(payloadSize)); + return {}; + } + + this->sendWebSocketString(message, Opcode::Pong); + + return {}; +} + +ExceptionOr<void> WebSocket::pong(ArrayBuffer& binaryData) +{ + LOG(Network, "WebSocket %p pong() Sending ArrayBuffer %p", this, &binaryData); + if (m_state == CONNECTING) + return Exception { InvalidStateError }; + + if (m_state == CLOSING || m_state == CLOSED) { + unsigned payloadSize = binaryData.byteLength(); + m_bufferedAmountAfterClose = saturateAdd(m_bufferedAmountAfterClose, payloadSize); + m_bufferedAmountAfterClose = saturateAdd(m_bufferedAmountAfterClose, getFramingOverhead(payloadSize)); + return {}; + } + + char* data = static_cast<char*>(binaryData.data()); + size_t length = binaryData.byteLength(); + this->sendWebSocketData(data, length, Opcode::Pong); + + return {}; +} + +ExceptionOr<void> WebSocket::pong(ArrayBufferView& arrayBufferView) +{ + LOG(Network, "WebSocket %p pong() Sending ArrayBufferView %p", this, &arrayBufferView); + + if (m_state == CONNECTING) + return Exception { InvalidStateError }; + + if (m_state == CLOSING || m_state == CLOSED) { + unsigned payloadSize = arrayBufferView.byteLength(); + m_bufferedAmountAfterClose = saturateAdd(m_bufferedAmountAfterClose, payloadSize); + m_bufferedAmountAfterClose = saturateAdd(m_bufferedAmountAfterClose, getFramingOverhead(payloadSize)); + return {}; + } + + auto buffer = arrayBufferView.unsharedBuffer().get(); + char* baseAddress = reinterpret_cast<char*>(buffer->data()) + arrayBufferView.byteOffset(); + size_t length = arrayBufferView.byteLength(); + this->sendWebSocketData(baseAddress, length, Opcode::Pong); + + return {}; +} + const URL& WebSocket::url() const { return m_url; @@ -829,7 +1032,7 @@ void WebSocket::didReceiveMessage(String&& message) // }); } -void WebSocket::didReceiveBinaryData(Vector<uint8_t>&& binaryData) +void WebSocket::didReceiveBinaryData(const AtomString& eventName, Vector<uint8_t>&& binaryData) { // LOG(Network, "WebSocket %p didReceiveBinaryData() %u byte binary message", this, static_cast<unsigned>(binaryData.size())); // queueTaskKeepingObjectAlive(*this, TaskSource::WebSocket, [this, binaryData = WTFMove(binaryData)]() mutable { @@ -840,17 +1043,16 @@ void WebSocket::didReceiveBinaryData(Vector<uint8_t>&& binaryData) // if (auto* inspector = m_channel->channelInspector()) // inspector->didReceiveWebSocketFrame(WebSocketChannelInspector::createFrame(binaryData.data(), binaryData.size(), WebSocketFrame::OpCode::OpCodeBinary)); // } - switch (m_binaryType) { // case BinaryType::Blob: // // FIXME: We just received the data from NetworkProcess, and are sending it back. This is inefficient. // dispatchEvent(MessageEvent::create(Blob::create(scriptExecutionContext(), WTFMove(binaryData), emptyString()), SecurityOrigin::create(m_url)->toString())); // break; case BinaryType::ArrayBuffer: { - if (this->hasEventListeners("message"_s)) { + if (this->hasEventListeners(eventName)) { // the main reason for dispatching on a separate tick is to handle when you haven't yet attached an event listener this->incPendingActivityCount(); - dispatchEvent(MessageEvent::create(ArrayBuffer::create(binaryData.data(), binaryData.size()), m_url.string())); + dispatchEvent(MessageEvent::create(eventName, ArrayBuffer::create(binaryData.data(), binaryData.size()), m_url.string())); this->decPendingActivityCount(); return; } @@ -858,9 +1060,9 @@ void WebSocket::didReceiveBinaryData(Vector<uint8_t>&& binaryData) if (auto* context = scriptExecutionContext()) { auto arrayBuffer = JSC::ArrayBuffer::create(binaryData.data(), binaryData.size()); this->incPendingActivityCount(); - context->postTask([this, buffer = WTFMove(arrayBuffer), protectedThis = Ref { *this }](ScriptExecutionContext& context) { + context->postTask([this, name = WTFMove(eventName), buffer = WTFMove(arrayBuffer), protectedThis = Ref { *this }](ScriptExecutionContext& context) { ASSERT(scriptExecutionContext()); - protectedThis->dispatchEvent(MessageEvent::create(buffer, m_url.string())); + protectedThis->dispatchEvent(MessageEvent::create(name, buffer, m_url.string())); protectedThis->decPendingActivityCount(); }); } @@ -869,7 +1071,7 @@ void WebSocket::didReceiveBinaryData(Vector<uint8_t>&& binaryData) } case BinaryType::NodeBuffer: { - if (this->hasEventListeners("message"_s)) { + if (this->hasEventListeners(eventName)) { // the main reason for dispatching on a separate tick is to handle when you haven't yet attached an event listener this->incPendingActivityCount(); JSUint8Array* buffer = jsCast<JSUint8Array*>(JSValue::decode(JSBuffer__bufferFromLength(scriptExecutionContext()->jsGlobalObject(), binaryData.size()))); @@ -880,7 +1082,7 @@ void WebSocket::didReceiveBinaryData(Vector<uint8_t>&& binaryData) init.data = buffer; init.origin = this->m_url.string(); - dispatchEvent(MessageEvent::create(eventNames().messageEvent, WTFMove(init), EventIsTrusted::Yes)); + dispatchEvent(MessageEvent::create(eventName, WTFMove(init), EventIsTrusted::Yes)); this->decPendingActivityCount(); return; } @@ -890,7 +1092,7 @@ void WebSocket::didReceiveBinaryData(Vector<uint8_t>&& binaryData) this->incPendingActivityCount(); - context->postTask([this, buffer = WTFMove(arrayBuffer), protectedThis = Ref { *this }](ScriptExecutionContext& context) { + context->postTask([this, name = WTFMove(eventName), buffer = WTFMove(arrayBuffer), protectedThis = Ref { *this }](ScriptExecutionContext& context) { ASSERT(scriptExecutionContext()); size_t length = buffer->byteLength(); JSUint8Array* uint8array = JSUint8Array::create( @@ -903,7 +1105,7 @@ void WebSocket::didReceiveBinaryData(Vector<uint8_t>&& binaryData) MessageEvent::Init init; init.data = uint8array; init.origin = protectedThis->m_url.string(); - protectedThis->dispatchEvent(MessageEvent::create(eventNames().messageEvent, WTFMove(init), EventIsTrusted::Yes)); + protectedThis->dispatchEvent(MessageEvent::create(name, WTFMove(init), EventIsTrusted::Yes)); protectedThis->decPendingActivityCount(); }); } @@ -914,7 +1116,7 @@ void WebSocket::didReceiveBinaryData(Vector<uint8_t>&& binaryData) // }); } -void WebSocket::didReceiveMessageError(unsigned short code, WTF::String reason) +void WebSocket::didReceiveClose(CleanStatus wasClean, unsigned short code, WTF::String reason) { // LOG(Network, "WebSocket %p didReceiveErrorMessage()", this); // queueTaskKeepingObjectAlive(*this, TaskSource::WebSocket, [this, reason = WTFMove(reason)] { @@ -924,7 +1126,7 @@ void WebSocket::didReceiveMessageError(unsigned short code, WTF::String reason) if (auto* context = scriptExecutionContext()) { this->incPendingActivityCount(); // https://html.spec.whatwg.org/multipage/web-sockets.html#feedback-from-the-protocol:concept-websocket-closed, we should synchronously fire a close event. - dispatchEvent(CloseEvent::create(code < 1002, code, reason)); + dispatchEvent(CloseEvent::create(wasClean == CleanStatus::Clean, code, reason)); this->decPendingActivityCount(); } } @@ -1051,158 +1253,158 @@ void WebSocket::didFailWithErrorCode(int32_t code) // invalid_response case 1: { static NeverDestroyed<String> message = MAKE_STATIC_STRING_IMPL("Invalid response"); - didReceiveMessageError(1002, message); + didReceiveClose(CleanStatus::NotClean, 1002, message); break; } // expected_101_status_code case 2: { static NeverDestroyed<String> message = MAKE_STATIC_STRING_IMPL("Expected 101 status code"); - didReceiveMessageError(1002, message); + didReceiveClose(CleanStatus::NotClean, 1002, message); break; } // missing_upgrade_header case 3: { static NeverDestroyed<String> message = MAKE_STATIC_STRING_IMPL("Missing upgrade header"); - didReceiveMessageError(1002, message); + didReceiveClose(CleanStatus::NotClean, 1002, message); break; } // missing_connection_header case 4: { static NeverDestroyed<String> message = MAKE_STATIC_STRING_IMPL("Missing connection header"); - didReceiveMessageError(1002, message); + didReceiveClose(CleanStatus::NotClean, 1002, message); break; } // missing_websocket_accept_header case 5: { static NeverDestroyed<String> message = MAKE_STATIC_STRING_IMPL("Missing websocket accept header"); - didReceiveMessageError(1002, message); + didReceiveClose(CleanStatus::NotClean, 1002, message); break; } // invalid_upgrade_header case 6: { static NeverDestroyed<String> message = MAKE_STATIC_STRING_IMPL("Invalid upgrade header"); - didReceiveMessageError(1002, message); + didReceiveClose(CleanStatus::NotClean, 1002, message); break; } // invalid_connection_header case 7: { static NeverDestroyed<String> message = MAKE_STATIC_STRING_IMPL("Invalid connection header"); - didReceiveMessageError(1002, message); + didReceiveClose(CleanStatus::NotClean, 1002, message); break; } // invalid_websocket_version case 8: { static NeverDestroyed<String> message = MAKE_STATIC_STRING_IMPL("Invalid websocket version"); - didReceiveMessageError(1002, message); + didReceiveClose(CleanStatus::NotClean, 1002, message); break; } // mismatch_websocket_accept_header case 9: { static NeverDestroyed<String> message = MAKE_STATIC_STRING_IMPL("Mismatch websocket accept header"); - didReceiveMessageError(1002, message); + didReceiveClose(CleanStatus::NotClean, 1002, message); break; } // missing_client_protocol case 10: { static NeverDestroyed<String> message = MAKE_STATIC_STRING_IMPL("Missing client protocol"); - didReceiveMessageError(1002, message); + didReceiveClose(CleanStatus::Clean, 1002, message); break; } // mismatch_client_protocol case 11: { static NeverDestroyed<String> message = MAKE_STATIC_STRING_IMPL("Mismatch client protocol"); - didReceiveMessageError(1002, message); + didReceiveClose(CleanStatus::Clean, 1002, message); break; } // timeout case 12: { static NeverDestroyed<String> message = MAKE_STATIC_STRING_IMPL("Timeout"); - didReceiveMessageError(1013, message); + didReceiveClose(CleanStatus::Clean, 1013, message); break; } // closed case 13: { static NeverDestroyed<String> message = MAKE_STATIC_STRING_IMPL("Closed by client"); - didReceiveMessageError(1000, message); + didReceiveClose(CleanStatus::Clean, 1000, message); break; } // failed_to_write case 14: { static NeverDestroyed<String> message = MAKE_STATIC_STRING_IMPL("Failed to write"); - didReceiveMessageError(1006, message); + didReceiveClose(CleanStatus::NotClean, 1006, message); break; } // failed_to_connect case 15: { static NeverDestroyed<String> message = MAKE_STATIC_STRING_IMPL("Failed to connect"); - didReceiveMessageError(1006, message); + didReceiveClose(CleanStatus::NotClean, 1006, message); break; } // headers_too_large case 16: { static NeverDestroyed<String> message = MAKE_STATIC_STRING_IMPL("Headers too large"); - didReceiveMessageError(1007, message); + didReceiveClose(CleanStatus::NotClean, 1007, message); break; } // ended case 17: { - static NeverDestroyed<String> message = MAKE_STATIC_STRING_IMPL("Closed by server"); - didReceiveMessageError(1001, message); + static NeverDestroyed<String> message = MAKE_STATIC_STRING_IMPL("Connection ended"); + didReceiveClose(CleanStatus::NotClean, 1006, message); break; } // failed_to_allocate_memory case 18: { static NeverDestroyed<String> message = MAKE_STATIC_STRING_IMPL("Failed to allocate memory"); - didReceiveMessageError(1001, message); + didReceiveClose(CleanStatus::NotClean, 1001, message); break; } // control_frame_is_fragmented case 19: { static NeverDestroyed<String> message = MAKE_STATIC_STRING_IMPL("Protocol error - control frame is fragmented"); - didReceiveMessageError(1002, message); + didReceiveClose(CleanStatus::NotClean, 1002, message); break; } // invalid_control_frame case 20: { static NeverDestroyed<String> message = MAKE_STATIC_STRING_IMPL("Protocol error - invalid control frame"); - didReceiveMessageError(1002, message); + didReceiveClose(CleanStatus::NotClean, 1002, message); break; } // compression_unsupported case 21: { static NeverDestroyed<String> message = MAKE_STATIC_STRING_IMPL("Compression not implemented yet"); - didReceiveMessageError(1011, message); + didReceiveClose(CleanStatus::Clean, 1011, message); break; } // unexpected_mask_from_server case 22: { static NeverDestroyed<String> message = MAKE_STATIC_STRING_IMPL("Protocol error - unexpected mask from server"); - didReceiveMessageError(1002, message); + didReceiveClose(CleanStatus::NotClean, 1002, message); break; } // expected_control_frame case 23: { static NeverDestroyed<String> message = MAKE_STATIC_STRING_IMPL("Protocol error - expected control frame"); - didReceiveMessageError(1002, message); + didReceiveClose(CleanStatus::NotClean, 1002, message); break; } // unsupported_control_frame case 24: { static NeverDestroyed<String> message = MAKE_STATIC_STRING_IMPL("Protocol error - unsupported control frame"); - didReceiveMessageError(1002, message); + didReceiveClose(CleanStatus::NotClean, 1002, message); break; } // unexpected_opcode case 25: { static NeverDestroyed<String> message = MAKE_STATIC_STRING_IMPL("Protocol error - unexpected opcode"); - didReceiveMessageError(1002, message); + didReceiveClose(CleanStatus::NotClean, 1002, message); break; } // invalid_utf8 case 26: { static NeverDestroyed<String> message = MAKE_STATIC_STRING_IMPL("Server sent invalid UTF8"); - didReceiveMessageError(1003, message); + didReceiveClose(CleanStatus::NotClean, 1003, message); break; } } @@ -1225,19 +1427,37 @@ extern "C" void WebSocket__didConnect(WebCore::WebSocket* webSocket, us_socket_t { webSocket->didConnect(socket, bufferedData, len); } -extern "C" void WebSocket__didCloseWithErrorCode(WebCore::WebSocket* webSocket, int32_t errorCode) +extern "C" void WebSocket__didAbruptClose(WebCore::WebSocket* webSocket, int32_t errorCode) { webSocket->didFailWithErrorCode(errorCode); } +extern "C" void WebSocket__didClose(WebCore::WebSocket* webSocket, uint16_t errorCode, const BunString *reason) +{ + WTF::String wtf_reason = Bun::toWTFString(*reason); + webSocket->didClose(0, errorCode, WTFMove(wtf_reason)); +} extern "C" void WebSocket__didReceiveText(WebCore::WebSocket* webSocket, bool clone, const ZigString* str) { WTF::String wtf_str = clone ? Zig::toStringCopy(*str) : Zig::toString(*str); webSocket->didReceiveMessage(WTFMove(wtf_str)); } -extern "C" void WebSocket__didReceiveBytes(WebCore::WebSocket* webSocket, uint8_t* bytes, size_t len) +extern "C" void WebSocket__didReceiveBytes(WebCore::WebSocket* webSocket, uint8_t* bytes, size_t len, const uint8_t op) { - webSocket->didReceiveBinaryData({ bytes, len }); + auto opcode = static_cast<WebCore::WebSocket::Opcode>(op); + switch (opcode) { + case WebCore::WebSocket::Opcode::Binary: + webSocket->didReceiveBinaryData("message"_s, { bytes, len }); + break; + case WebCore::WebSocket::Opcode::Ping: + webSocket->didReceiveBinaryData("ping"_s, { bytes, len }); + break; + case WebCore::WebSocket::Opcode::Pong: + webSocket->didReceiveBinaryData("pong"_s, { bytes, len }); + break; + default: + break; + } } extern "C" void WebSocket__incrementPendingActivity(WebCore::WebSocket* webSocket) diff --git a/src/bun.js/bindings/webcore/WebSocket.h b/src/bun.js/bindings/webcore/WebSocket.h index 846bd186b..cf18bd40d 100644 --- a/src/bun.js/bindings/webcore/WebSocket.h +++ b/src/bun.js/bindings/webcore/WebSocket.h @@ -70,6 +70,20 @@ public: CLOSED = 3, }; + enum Opcode : unsigned char { + Continue = 0x0, + Text = 0x1, + Binary = 0x2, + Close = 0x8, + Ping = 0x9, + Pong = 0xA, + }; + + enum CleanStatus { + NotClean = 0, + Clean = 1, + }; + ExceptionOr<void> connect(const String& url); ExceptionOr<void> connect(const String& url, const String& protocol); ExceptionOr<void> connect(const String& url, const Vector<String>& protocols); @@ -80,7 +94,20 @@ public: ExceptionOr<void> send(JSC::ArrayBufferView&); // ExceptionOr<void> send(Blob&); + ExceptionOr<void> ping(); + ExceptionOr<void> ping(const String& message); + ExceptionOr<void> ping(JSC::ArrayBuffer&); + ExceptionOr<void> ping(JSC::ArrayBufferView&); + // ExceptionOr<void> ping(Blob&); + + ExceptionOr<void> pong(); + ExceptionOr<void> pong(const String& message); + ExceptionOr<void> pong(JSC::ArrayBuffer&); + ExceptionOr<void> pong(JSC::ArrayBufferView&); + // ExceptionOr<void> ping(Blob&); + ExceptionOr<void> close(std::optional<unsigned short> code, const String& reason); + ExceptionOr<void> terminate(); const URL& url() const; State readyState() const; @@ -103,7 +130,7 @@ public: void didReceiveMessage(String&& message); void didReceiveData(const char* data, size_t length); - void didReceiveBinaryData(Vector<uint8_t>&&); + void didReceiveBinaryData(const AtomString& eventName, Vector<uint8_t>&& binaryData); void updateHasPendingActivity(); bool hasPendingActivity() const @@ -154,12 +181,12 @@ private: void refEventTarget() final { ref(); } void derefEventTarget() final { deref(); } - void didReceiveMessageError(unsigned short code, WTF::String reason); + void didReceiveClose(CleanStatus wasClean, unsigned short code, WTF::String reason); void didUpdateBufferedAmount(unsigned bufferedAmount); void didStartClosingHandshake(); - void sendWebSocketString(const String& message); - void sendWebSocketData(const char* data, size_t length); + void sendWebSocketString(const String& message, const Opcode opcode); + void sendWebSocketData(const char* data, size_t length, const Opcode opcode); void failAsynchronously(); @@ -172,7 +199,12 @@ private: URL m_url; unsigned m_bufferedAmount { 0 }; unsigned m_bufferedAmountAfterClose { 0 }; - BinaryType m_binaryType { BinaryType::ArrayBuffer }; + // In browsers, the default is Blob, however most applications + // immediately change the default to ArrayBuffer. + // + // And since we know the typical usage is to override the default, + // we set NodeBuffer as the default to match the default of ServerWebSocket. + BinaryType m_binaryType { BinaryType::NodeBuffer }; String m_subprotocol; String m_extensions; void* m_upgradeClient { nullptr }; diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig index 7d2435823..beb2d1856 100644 --- a/src/bun.js/javascript.zig +++ b/src/bun.js/javascript.zig @@ -350,12 +350,14 @@ pub const ExitHandler = struct { extern fn Bun__closeAllSQLiteDatabasesForTermination() void; pub fn dispatchOnExit(this: *ExitHandler) void { + JSC.markBinding(@src()); var vm = @fieldParentPtr(VirtualMachine, "exit_handler", this); Process__dispatchOnExit(vm.global, this.exit_code); Bun__closeAllSQLiteDatabasesForTermination(); } pub fn dispatchOnBeforeExit(this: *ExitHandler) void { + JSC.markBinding(@src()); var vm = @fieldParentPtr(VirtualMachine, "exit_handler", this); Process__dispatchOnBeforeExit(vm.global, this.exit_code); } diff --git a/src/http/websocket_http_client.zig b/src/http/websocket_http_client.zig index ae8e40763..80f29525c 100644 --- a/src/http/websocket_http_client.zig +++ b/src/http/websocket_http_client.zig @@ -79,11 +79,16 @@ fn buildRequestBody( if (client_protocol.len > 0) client_protocol_hash.* = bun.hash(static_headers[1].value); - const headers_ = static_headers[0 .. 1 + @as(usize, @intFromBool(client_protocol.len > 0))]; + const pathname_ = pathname.toSlice(allocator); + const host_ = host.toSlice(allocator); + defer { + pathname_.deinit(); + host_.deinit(); + } - const pathname_ = pathname.slice(); - const host_ = host.slice(); + const headers_ = static_headers[0 .. 1 + @as(usize, @intFromBool(client_protocol.len > 0))]; const pico_headers = PicoHTTP.Headers{ .headers = headers_ }; + return try std.fmt.allocPrint( allocator, "GET {s} HTTP/1.1\r\n" ++ @@ -93,10 +98,10 @@ fn buildRequestBody( "Connection: Upgrade\r\n" ++ "Upgrade: websocket\r\n" ++ "Sec-WebSocket-Version: 13\r\n" ++ - "{any}" ++ - "{any}" ++ + "{s}" ++ + "{s}" ++ "\r\n", - .{ pathname_, host_, pico_headers, extra_headers }, + .{ pathname_.slice(), host_.slice(), pico_headers, extra_headers }, ); } @@ -137,12 +142,14 @@ const CppWebSocket = opaque { buffered_data: ?[*]u8, buffered_len: usize, ) void; - extern fn WebSocket__didCloseWithErrorCode(websocket_context: *CppWebSocket, reason: ErrorCode) void; + extern fn WebSocket__didAbruptClose(websocket_context: *CppWebSocket, reason: ErrorCode) void; + extern fn WebSocket__didClose(websocket_context: *CppWebSocket, code: u16, reason: *const bun.String) void; extern fn WebSocket__didReceiveText(websocket_context: *CppWebSocket, clone: bool, text: *const JSC.ZigString) void; - extern fn WebSocket__didReceiveBytes(websocket_context: *CppWebSocket, bytes: [*]const u8, byte_len: usize) void; + extern fn WebSocket__didReceiveBytes(websocket_context: *CppWebSocket, bytes: [*]const u8, byte_len: usize, opcode: u8) void; pub const didConnect = WebSocket__didConnect; - pub const didCloseWithErrorCode = WebSocket__didCloseWithErrorCode; + pub const didAbruptClose = WebSocket__didAbruptClose; + pub const didClose = WebSocket__didClose; pub const didReceiveText = WebSocket__didReceiveText; pub const didReceiveBytes = WebSocket__didReceiveBytes; extern fn WebSocket__incrementPendingActivity(websocket_context: *CppWebSocket) void; @@ -307,7 +314,7 @@ pub fn NewHTTPUpgradeClient(comptime ssl: bool) type { JSC.markBinding(@src()); if (this.outgoing_websocket) |ws| { this.outgoing_websocket = null; - ws.didCloseWithErrorCode(code); + ws.didAbruptClose(code); } this.cancel(); @@ -319,7 +326,7 @@ pub fn NewHTTPUpgradeClient(comptime ssl: bool) type { this.clearData(); if (this.outgoing_websocket) |ws| { this.outgoing_websocket = null; - ws.didCloseWithErrorCode(ErrorCode.ended); + ws.didAbruptClose(ErrorCode.ended); } } @@ -728,10 +735,10 @@ fn parseWebSocketHeader( return .extended_payload_length_64 else return .fail, - .Close => ReceiveState.close, - .Ping => ReceiveState.ping, - .Pong => ReceiveState.pong, - else => ReceiveState.fail, + .Close => .close, + .Ping => .ping, + .Pong => .pong, + else => .fail, }; } @@ -762,7 +769,7 @@ const Copy = union(enum) { } } - pub fn copy(this: @This(), globalThis: *JSC.JSGlobalObject, buf: []u8, content_byte_len: usize) void { + pub fn copy(this: @This(), globalThis: *JSC.JSGlobalObject, buf: []u8, content_byte_len: usize, opcode: Opcode) void { if (this == .raw) { std.debug.assert(buf.len >= this.raw.len); std.debug.assert(buf.ptr != this.raw.ptr); @@ -793,6 +800,7 @@ const Copy = union(enum) { header.mask = true; header.compressed = false; header.final = true; + header.opcode = opcode; std.debug.assert(WebsocketHeader.frameSizeIncludingMask(content_byte_len) == buf.len); @@ -803,7 +811,6 @@ const Copy = union(enum) { std.debug.assert(@as(usize, encode_into_result.written) == content_byte_len); std.debug.assert(@as(usize, encode_into_result.read) == utf16.len); header.len = WebsocketHeader.packLength(encode_into_result.written); - header.opcode = Opcode.Text; var fib = std.io.fixedBufferStream(buf); header.writeHeader(fib.writer(), encode_into_result.written) catch unreachable; @@ -817,14 +824,12 @@ const Copy = union(enum) { std.debug.assert(@as(usize, encode_into_result.read) == latin1.len); header.len = WebsocketHeader.packLength(encode_into_result.written); - header.opcode = Opcode.Text; var fib = std.io.fixedBufferStream(buf); header.writeHeader(fib.writer(), encode_into_result.written) catch unreachable; Mask.fill(globalThis, buf[mask_offset..][0..4], to_mask[0..content_byte_len], to_mask[0..content_byte_len]); }, .bytes => |bytes| { header.len = WebsocketHeader.packLength(bytes.len); - header.opcode = Opcode.Binary; var fib = std.io.fixedBufferStream(buf); header.writeHeader(fib.writer(), bytes.len) catch unreachable; Mask.fill(globalThis, buf[mask_offset..][0..4], to_mask[0..content_byte_len], bytes); @@ -846,6 +851,7 @@ pub fn NewWebSocketClient(comptime ssl: bool) type { ping_frame_bytes: [128 + 6]u8 = [_]u8{0} ** (128 + 6), ping_len: u8 = 0, + ping_received: bool = false, receive_frame: usize = 0, receive_body_remain: usize = 0, @@ -899,6 +905,7 @@ pub fn NewWebSocketClient(comptime ssl: bool) type { this.poll_ref.unrefOnNextTick(this.globalThis.bunVM()); this.clearReceiveBuffers(true); this.clearSendBuffers(true); + this.ping_received = false; this.ping_len = 0; this.receive_pending_chunk_len = 0; } @@ -921,8 +928,7 @@ pub fn NewWebSocketClient(comptime ssl: bool) type { if (this.outgoing_websocket) |ws| { this.outgoing_websocket = null; log("fail ({s})", .{@tagName(code)}); - - ws.didCloseWithErrorCode(code); + ws.didAbruptClose(code); } this.cancel(); @@ -937,7 +943,7 @@ pub fn NewWebSocketClient(comptime ssl: bool) type { if (success == 0) { if (this.outgoing_websocket) |ws| { this.outgoing_websocket = null; - ws.didCloseWithErrorCode(ErrorCode.failed_to_connect); + ws.didAbruptClose(ErrorCode.failed_to_connect); } } } @@ -947,7 +953,7 @@ pub fn NewWebSocketClient(comptime ssl: bool) type { this.clearData(); if (this.outgoing_websocket) |ws| { this.outgoing_websocket = null; - ws.didCloseWithErrorCode(ErrorCode.ended); + ws.didAbruptClose(ErrorCode.ended); } } @@ -1002,16 +1008,15 @@ pub fn NewWebSocketClient(comptime ssl: bool) type { out.didReceiveText(true, &outstring); } }, - .Binary => { + .Binary, .Ping, .Pong => { JSC.markBinding(@src()); - out.didReceiveBytes(data_.ptr, data_.len); + out.didReceiveBytes(data_.ptr, data_.len, @as(u8, @intFromEnum(kind))); }, else => unreachable, } } pub fn consume(this: *WebSocket, data_: []const u8, left_in_fragment: usize, kind: Opcode, is_final: bool) usize { - std.debug.assert(kind == .Text or kind == .Binary); std.debug.assert(data_.len <= left_in_fragment); // did all the data fit in the buffer? @@ -1074,9 +1079,9 @@ pub fn NewWebSocketClient(comptime ssl: bool) type { // if we receive multiple pings in a row // we just send back the last one - if (this.ping_len > 0) { + if (this.ping_received) { _ = this.sendPong(socket); - this.ping_len = 0; + this.ping_received = false; } } } @@ -1214,10 +1219,12 @@ pub fn NewWebSocketClient(comptime ssl: bool) type { break; } }, - .ping => { const ping_len = @min(data.len, @min(receive_body_remain, 125)); this.ping_len = ping_len; + this.ping_received = true; + + this.dispatchData(data[0..ping_len], .Ping); if (ping_len > 0) { @memcpy(this.ping_frame_bytes[6..][0..ping_len], data[0..ping_len]); @@ -1232,9 +1239,14 @@ pub fn NewWebSocketClient(comptime ssl: bool) type { }, .pong => { const pong_len = @min(data.len, @min(receive_body_remain, this.ping_frame_bytes.len)); + + this.dispatchData(data[0..pong_len], .Pong); + data = data[pong_len..]; receive_state = .need_header; + receive_body_remain = 0; receiving_type = last_receive_data_type; + if (data.len == 0) break; }, .need_body => { @@ -1318,16 +1330,16 @@ pub fn NewWebSocketClient(comptime ssl: bool) type { } fn copyToSendBuffer(this: *WebSocket, bytes: []const u8, do_write: bool, is_closing: bool) bool { - return this.sendData(.{ .raw = bytes }, do_write, is_closing); + return this.sendData(.{ .raw = bytes }, do_write, is_closing, .Binary); } - fn sendData(this: *WebSocket, bytes: Copy, do_write: bool, is_closing: bool) bool { + fn sendData(this: *WebSocket, bytes: Copy, do_write: bool, is_closing: bool, opcode: Opcode) bool { var content_byte_len: usize = 0; const write_len = bytes.len(&content_byte_len); std.debug.assert(write_len > 0); var writable = this.send_buffer.writableWithSize(write_len) catch unreachable; - bytes.copy(this.globalThis, writable[0..write_len], content_byte_len); + bytes.copy(this.globalThis, writable[0..write_len], content_byte_len, opcode); this.send_buffer.update(write_len); if (do_write) { @@ -1372,7 +1384,7 @@ pub fn NewWebSocketClient(comptime ssl: bool) type { fn sendPong(this: *WebSocket, socket: Socket) bool { if (socket.isClosed() or socket.isShutdown()) { - this.dispatchClose(); + this.dispatchAbruptClose(); return false; } @@ -1403,7 +1415,7 @@ pub fn NewWebSocketClient(comptime ssl: bool) type { ) void { log("Sending close with code {d}", .{code}); if (socket.isClosed() or socket.isShutdown()) { - this.dispatchClose(); + this.dispatchAbruptClose(); this.clearData(); return; } @@ -1419,8 +1431,12 @@ pub fn NewWebSocketClient(comptime ssl: bool) type { var mask_buf: *[4]u8 = final_body_bytes[2..6]; std.mem.writeIntSliceBig(u16, final_body_bytes[6..8], code); + var reason = bun.String.empty; if (body) |data| { - if (body_len > 0) @memcpy(final_body_bytes[8..][0..body_len], data[0..body_len]); + if (body_len > 0) { + reason = bun.String.create(data[0..body_len]); + @memcpy(final_body_bytes[8..][0..body_len], data[0..body_len]); + } } // we must mask the code @@ -1428,7 +1444,7 @@ pub fn NewWebSocketClient(comptime ssl: bool) type { Mask.fill(this.globalThis, mask_buf, slice[6..], slice[6..]); if (this.enqueueEncodedBytesMaybeFinal(socket, slice, true)) { - this.dispatchClose(); + this.dispatchClose(code, &reason); this.clearData(); } } @@ -1466,37 +1482,41 @@ pub fn NewWebSocketClient(comptime ssl: bool) type { this: *WebSocket, ptr: [*]const u8, len: usize, + op: u8, ) callconv(.C) void { if (this.tcp.isClosed() or this.tcp.isShutdown()) { - this.dispatchClose(); + this.dispatchAbruptClose(); return; } + const opcode = @enumFromInt(Opcode, @truncate(u4, op)); const slice = ptr[0..len]; const bytes = Copy{ .bytes = slice }; // fast path: small frame, no backpressure, attempt to send without allocating const frame_size = WebsocketHeader.frameSizeIncludingMask(len); if (!this.hasBackpressure() and frame_size < stack_frame_size) { var inline_buf: [stack_frame_size]u8 = undefined; - bytes.copy(this.globalThis, inline_buf[0..frame_size], slice.len); + bytes.copy(this.globalThis, inline_buf[0..frame_size], slice.len, opcode); _ = this.enqueueEncodedBytes(this.tcp, inline_buf[0..frame_size]); return; } - _ = this.sendData(bytes, !this.hasBackpressure(), false); + _ = this.sendData(bytes, !this.hasBackpressure(), false, opcode); } pub fn writeString( this: *WebSocket, str_: *const JSC.ZigString, + op: u8, ) callconv(.C) void { const str = str_.*; if (this.tcp.isClosed() or this.tcp.isShutdown()) { - this.dispatchClose(); + this.dispatchAbruptClose(); return; } // Note: 0 is valid + const opcode = @enumFromInt(Opcode, @truncate(u4, op)); { var inline_buf: [stack_frame_size]u8 = undefined; @@ -1506,7 +1526,7 @@ pub fn NewWebSocketClient(comptime ssl: bool) type { var byte_len: usize = 0; const frame_size = bytes.len(&byte_len); if (!this.hasBackpressure() and frame_size < stack_frame_size) { - bytes.copy(this.globalThis, inline_buf[0..frame_size], byte_len); + bytes.copy(this.globalThis, inline_buf[0..frame_size], byte_len, opcode); _ = this.enqueueEncodedBytes(this.tcp, inline_buf[0..frame_size]); return; } @@ -1516,7 +1536,7 @@ pub fn NewWebSocketClient(comptime ssl: bool) type { var byte_len: usize = 0; const frame_size = bytes.len(&byte_len); std.debug.assert(frame_size <= stack_frame_size); - bytes.copy(this.globalThis, inline_buf[0..frame_size], byte_len); + bytes.copy(this.globalThis, inline_buf[0..frame_size], byte_len, opcode); _ = this.enqueueEncodedBytes(this.tcp, inline_buf[0..frame_size]); return; } @@ -1529,15 +1549,24 @@ pub fn NewWebSocketClient(comptime ssl: bool) type { Copy{ .latin1 = str.slice() }, !this.hasBackpressure(), false, + opcode, ); } - fn dispatchClose(this: *WebSocket) void { + fn dispatchAbruptClose(this: *WebSocket) void { var out = this.outgoing_websocket orelse return; this.poll_ref.unrefOnNextTick(this.globalThis.bunVM()); JSC.markBinding(@src()); this.outgoing_websocket = null; - out.didCloseWithErrorCode(ErrorCode.closed); + out.didAbruptClose(ErrorCode.closed); + } + + fn dispatchClose(this: *WebSocket, code: u16, reason: *const bun.String) void { + var out = this.outgoing_websocket orelse return; + this.poll_ref.unrefOnNextTick(this.globalThis.bunVM()); + JSC.markBinding(@src()); + this.outgoing_websocket = null; + out.didClose(code, reason); } pub fn close(this: *WebSocket, code: u16, reason: ?*const JSC.ZigString) callconv(.C) void { @@ -1650,6 +1679,7 @@ pub fn NewWebSocketClient(comptime ssl: bool) type { .writeBinaryData = writeBinaryData, .writeString = writeString, .close = close, + .cancel = cancel, .register = register, .init = init, .finalize = finalize, @@ -1660,9 +1690,10 @@ pub fn NewWebSocketClient(comptime ssl: bool) type { @export(writeBinaryData, .{ .name = Export[0].symbol_name }); @export(writeString, .{ .name = Export[1].symbol_name }); @export(close, .{ .name = Export[2].symbol_name }); - @export(register, .{ .name = Export[3].symbol_name }); - @export(init, .{ .name = Export[4].symbol_name }); - @export(finalize, .{ .name = Export[5].symbol_name }); + @export(cancel, .{ .name = Export[3].symbol_name }); + @export(register, .{ .name = Export[4].symbol_name }); + @export(init, .{ .name = Export[5].symbol_name }); + @export(finalize, .{ .name = Export[6].symbol_name }); } } }; diff --git a/src/js/out/modules/thirdparty/ws.js b/src/js/out/modules/thirdparty/ws.js index 7a48da4c1..175ab5fa1 100644 --- a/src/js/out/modules/thirdparty/ws.js +++ b/src/js/out/modules/thirdparty/ws.js @@ -48,7 +48,14 @@ var emitWarning = function(type, message) { Error.captureStackTrace(err, abortHandshakeOrEmitwsClientError), server.emit("wsClientError", err, socket, req); } else abortHandshake(response, code, message); -}, kBunInternals = Symbol.for("::bunternal::"), readyStates = ["CONNECTING", "OPEN", "CLOSING", "CLOSED"], encoder = new TextEncoder, emittedWarnings = new Set; +}, kBunInternals = Symbol.for("::bunternal::"), readyStates = ["CONNECTING", "OPEN", "CLOSING", "CLOSED"], encoder = new TextEncoder, eventIds = { + open: 1, + close: 2, + message: 3, + error: 4, + ping: 5, + pong: 6 +}, emittedWarnings = new Set; class BunWebSocket extends EventEmitter { static CONNECTING = 0; @@ -59,51 +66,83 @@ class BunWebSocket extends EventEmitter { #paused = !1; #fragments = !1; #binaryType = "nodebuffer"; - readyState = BunWebSocket.CONNECTING; + #eventId = 0; constructor(url, protocols, options) { super(); let ws = this.#ws = new WebSocket(url, protocols); - ws.binaryType = "nodebuffer", ws.addEventListener("open", () => { - this.readyState = BunWebSocket.OPEN, this.emit("open"); - }), ws.addEventListener("error", (err) => { - this.readyState = BunWebSocket.CLOSED, this.emit("error", err); - }), ws.addEventListener("close", (ev) => { - this.readyState = BunWebSocket.CLOSED, this.emit("close", ev.code, ev.reason); - }), ws.addEventListener("message", (ev) => { - const isBinary = typeof ev.data !== "string"; - if (isBinary) - this.emit("message", this.#fragments ? [ev.data] : ev.data, isBinary); - else { - var encoded = encoder.encode(ev.data); - if (this.#binaryType !== "arraybuffer") - encoded = Buffer.from(encoded.buffer, encoded.byteOffset, encoded.byteLength); - this.emit("message", this.#fragments ? [encoded] : encoded, isBinary); - } - }); + ws.binaryType = "nodebuffer"; } on(event, listener) { - if (event === "unexpected-response" || event === "upgrade" || event === "ping" || event === "pong" || event === "redirect") + if (event === "unexpected-response" || event === "upgrade" || event === "redirect") emitWarning(event, "ws.WebSocket '" + event + "' event is not implemented in bun"); + const mask = 1 << eventIds[event]; + if (mask && (this.#eventId & mask) !== mask) { + if (this.#eventId |= mask, event === "open") + this.#ws.addEventListener("open", () => { + this.emit("open"); + }); + else if (event === "close") + this.#ws.addEventListener("close", ({ code, reason, wasClean }) => { + this.emit("close", code, reason, wasClean); + }); + else if (event === "message") + this.#ws.addEventListener("message", ({ data }) => { + const isBinary = typeof data !== "string"; + if (isBinary) + this.emit("message", this.#fragments ? [data] : data, isBinary); + else { + let encoded = encoder.encode(data); + if (this.#binaryType !== "arraybuffer") + encoded = Buffer.from(encoded.buffer, encoded.byteOffset, encoded.byteLength); + this.emit("message", this.#fragments ? [encoded] : encoded, isBinary); + } + }); + else if (event === "error") + this.#ws.addEventListener("error", (err) => { + this.emit("error", err); + }); + else if (event === "ping") + this.#ws.addEventListener("ping", ({ data }) => { + this.emit("ping", data); + }); + else if (event === "pong") + this.#ws.addEventListener("pong", ({ data }) => { + this.emit("pong", data); + }); + } return super.on(event, listener); } send(data, opts, cb) { - this.#ws.send(data, opts?.compress), typeof cb === "function" && cb(); + try { + this.#ws.send(data, opts?.compress); + } catch (error) { + typeof cb === "function" && cb(error); + return; + } + typeof cb === "function" && cb(); } close(code, reason) { this.#ws.close(code, reason); } + terminate() { + this.#ws.terminate(); + } + get url() { + return this.#ws.url; + } + get readyState() { + return this.#ws.readyState; + } get binaryType() { return this.#binaryType; } set binaryType(value) { - if (value) - this.#ws.binaryType = value; - } - set binaryType(value) { if (value === "nodebuffer" || value === "arraybuffer") this.#ws.binaryType = this.#binaryType = value, this.#fragments = !1; else if (value === "fragments") this.#ws.binaryType = "nodebuffer", this.#binaryType = "fragments", this.#fragments = !0; + else + throw new Error(`Invalid binaryType: ${value}`); } get protocol() { return this.#ws.protocol; @@ -148,35 +187,49 @@ class BunWebSocket extends EventEmitter { return this.#paused; } ping(data, mask, cb) { - if (this.readyState === BunWebSocket.CONNECTING) - throw new Error("WebSocket is not open: readyState 0 (CONNECTING)"); if (typeof data === "function") cb = data, data = mask = void 0; else if (typeof mask === "function") cb = mask, mask = void 0; if (typeof data === "number") data = data.toString(); - emitWarning("ping()", "ws.WebSocket.ping() is not implemented in bun"), typeof cb === "function" && cb(); + try { + this.#ws.ping(data); + } catch (error) { + typeof cb === "function" && cb(error); + return; + } + typeof cb === "function" && cb(); } pong(data, mask, cb) { - if (this.readyState === BunWebSocket.CONNECTING) - throw new Error("WebSocket is not open: readyState 0 (CONNECTING)"); if (typeof data === "function") cb = data, data = mask = void 0; else if (typeof mask === "function") cb = mask, mask = void 0; if (typeof data === "number") data = data.toString(); - emitWarning("pong()", "ws.WebSocket.pong() is not implemented in bun"), typeof cb === "function" && cb(); + try { + this.#ws.pong(data); + } catch (error) { + typeof cb === "function" && cb(error); + return; + } + typeof cb === "function" && cb(); } pause() { - if (this.readyState === WebSocket.CONNECTING || this.readyState === WebSocket.CLOSED) - return; + switch (this.readyState) { + case WebSocket.CONNECTING: + case WebSocket.CLOSED: + return; + } this.#paused = !0, emitWarning("pause()", "ws.WebSocket.pause() is not implemented in bun"); } resume() { - if (this.readyState === WebSocket.CONNECTING || this.readyState === WebSocket.CLOSED) - return; + switch (this.readyState) { + case WebSocket.CONNECTING: + case WebSocket.CLOSED: + return; + } this.#paused = !1, emitWarning("resume()", "ws.WebSocket.resume() is not implemented in bun"); } } diff --git a/src/js/thirdparty/ws.js b/src/js/thirdparty/ws.js index 5b27c5b50..e88ae6769 100644 --- a/src/js/thirdparty/ws.js +++ b/src/js/thirdparty/ws.js @@ -3,12 +3,20 @@ // this just wraps WebSocket to look like an EventEmitter // without actually using an EventEmitter polyfill -import { EventEmitter } from "events"; -import http from "http"; +import { EventEmitter } from "node:events"; +import http from "node:http"; const kBunInternals = Symbol.for("::bunternal::"); const readyStates = ["CONNECTING", "OPEN", "CLOSING", "CLOSED"]; const encoder = new TextEncoder(); +const eventIds = { + open: 1, + close: 2, + message: 3, + error: 4, + ping: 5, + pong: 6, +}; const emittedWarnings = new Set(); function emitWarning(type, message) { @@ -18,13 +26,8 @@ function emitWarning(type, message) { console.warn("[bun] Warning:", message); } -/* - * deviations: we do not implement these events - * - "unexpected-response" - * - "upgrade" - * - "ping" - * - "pong" - * - "redirect" +/** + * @link https://github.com/websockets/ws/blob/master/doc/ws.md#class-websocket */ class BunWebSocket extends EventEmitter { static CONNECTING = 0; @@ -36,54 +39,69 @@ class BunWebSocket extends EventEmitter { #paused = false; #fragments = false; #binaryType = "nodebuffer"; - readyState = BunWebSocket.CONNECTING; + + // Bitset to track whether event handlers are set. + #eventId = 0; constructor(url, protocols, options) { - // deviation: we don't support anything in `options` super(); let ws = (this.#ws = new WebSocket(url, protocols)); - ws.binaryType = "nodebuffer"; // bun's WebSocket supports "nodebuffer" - ws.addEventListener("open", () => { - this.readyState = BunWebSocket.OPEN; - this.emit("open"); - }); - ws.addEventListener("error", err => { - this.readyState = BunWebSocket.CLOSED; - this.emit("error", err); - }); - ws.addEventListener("close", ev => { - this.readyState = BunWebSocket.CLOSED; - this.emit("close", ev.code, ev.reason); - }); - ws.addEventListener("message", ev => { - const isBinary = typeof ev.data !== "string"; - if (isBinary) { - this.emit("message", this.#fragments ? [ev.data] : ev.data, isBinary); - } else { - var encoded = encoder.encode(ev.data); - if (this.#binaryType !== "arraybuffer") { - encoded = Buffer.from(encoded.buffer, encoded.byteOffset, encoded.byteLength); - } - this.emit("message", this.#fragments ? [encoded] : encoded, isBinary); - } - }); + ws.binaryType = "nodebuffer"; + // TODO: options } on(event, listener) { - if ( - event === "unexpected-response" || - event === "upgrade" || - event === "ping" || - event === "pong" || - event === "redirect" - ) { + if (event === "unexpected-response" || event === "upgrade" || event === "redirect") { emitWarning(event, "ws.WebSocket '" + event + "' event is not implemented in bun"); } + const mask = 1 << eventIds[event]; + if (mask && (this.#eventId & mask) !== mask) { + this.#eventId |= mask; + if (event === "open") { + this.#ws.addEventListener("open", () => { + this.emit("open"); + }); + } else if (event === "close") { + this.#ws.addEventListener("close", ({ code, reason, wasClean }) => { + this.emit("close", code, reason, wasClean); + }); + } else if (event === "message") { + this.#ws.addEventListener("message", ({ data }) => { + const isBinary = typeof data !== "string"; + if (isBinary) { + this.emit("message", this.#fragments ? [data] : data, isBinary); + } else { + let encoded = encoder.encode(data); + if (this.#binaryType !== "arraybuffer") { + encoded = Buffer.from(encoded.buffer, encoded.byteOffset, encoded.byteLength); + } + this.emit("message", this.#fragments ? [encoded] : encoded, isBinary); + } + }); + } else if (event === "error") { + this.#ws.addEventListener("error", (err) => { + this.emit("error", err); + }); + } else if (event === "ping") { + this.#ws.addEventListener("ping", ({ data }) => { + this.emit("ping", data); + }); + } else if (event === "pong") { + this.#ws.addEventListener("pong", ({ data }) => { + this.emit("pong", data); + }); + } + } return super.on(event, listener); } send(data, opts, cb) { - this.#ws.send(data, opts?.compress); + try { + this.#ws.send(data, opts?.compress); + } catch (error) { + typeof cb === "function" && cb(error); + return; + } // deviation: this should be called once the data is written, not immediately typeof cb === "function" && cb(); } @@ -92,12 +110,20 @@ class BunWebSocket extends EventEmitter { this.#ws.close(code, reason); } - get binaryType() { - return this.#binaryType; + terminate() { + this.#ws.terminate(); } - set binaryType(value) { - if (value) this.#ws.binaryType = value; + get url() { + return this.#ws.url; + } + + get readyState() { + return this.#ws.readyState; + } + + get binaryType() { + return this.#binaryType; } set binaryType(value) { @@ -108,6 +134,8 @@ class BunWebSocket extends EventEmitter { this.#ws.binaryType = "nodebuffer"; this.#binaryType = "fragments"; this.#fragments = true; + } else { + throw new Error(`Invalid binaryType: ${value}`); } } @@ -170,10 +198,6 @@ class BunWebSocket extends EventEmitter { } ping(data, mask, cb) { - if (this.readyState === BunWebSocket.CONNECTING) { - throw new Error("WebSocket is not open: readyState 0 (CONNECTING)"); - } - if (typeof data === "function") { cb = data; data = mask = undefined; @@ -184,16 +208,17 @@ class BunWebSocket extends EventEmitter { if (typeof data === "number") data = data.toString(); - // deviation: we don't support ping - emitWarning("ping()", "ws.WebSocket.ping() is not implemented in bun"); + try { + this.#ws.ping(data); + } catch (error) { + typeof cb === "function" && cb(error); + return; + } + typeof cb === "function" && cb(); } pong(data, mask, cb) { - if (this.readyState === BunWebSocket.CONNECTING) { - throw new Error("WebSocket is not open: readyState 0 (CONNECTING)"); - } - if (typeof data === "function") { cb = data; data = mask = undefined; @@ -204,14 +229,21 @@ class BunWebSocket extends EventEmitter { if (typeof data === "number") data = data.toString(); - // deviation: we don't support pong - emitWarning("pong()", "ws.WebSocket.pong() is not implemented in bun"); + try { + this.#ws.pong(data); + } catch (error) { + typeof cb === "function" && cb(error); + return; + } + typeof cb === "function" && cb(); } pause() { - if (this.readyState === WebSocket.CONNECTING || this.readyState === WebSocket.CLOSED) { - return; + switch (this.readyState) { + case WebSocket.CONNECTING: + case WebSocket.CLOSED: + return; } this.#paused = true; @@ -221,8 +253,10 @@ class BunWebSocket extends EventEmitter { } resume() { - if (this.readyState === WebSocket.CONNECTING || this.readyState === WebSocket.CLOSED) { - return; + switch (this.readyState) { + case WebSocket.CONNECTING: + case WebSocket.CLOSED: + return; } this.#paused = false; |