diff options
author | 2023-01-29 22:33:37 -0800 | |
---|---|---|
committer | 2023-01-29 22:33:37 -0800 | |
commit | 703bee976b9961a3cccc96c371c833546f3505d9 (patch) | |
tree | ce531762755f626837c593310e4264be1b1f9325 | |
parent | eb5105aa09767da7d015299f956ed0fdfdffb386 (diff) | |
download | bun-703bee976b9961a3cccc96c371c833546f3505d9.tar.gz bun-703bee976b9961a3cccc96c371c833546f3505d9.tar.zst bun-703bee976b9961a3cccc96c371c833546f3505d9.zip |
[breaking] Add `binaryType` option to Bun.connect & Bun.listen
This is a breaking change because the default is `Buffer`, but previously the default was `Uint8Array`. While `Buffer` is a subclass of `Uint8Array`, it still technically is a breaking change because `slice` in `Uint8Array` is not semantically identical to `slice` in `Buffer`
cc @colinhacks, the .d.ts changes I made here aren't great.
-rw-r--r-- | packages/bun-types/bun.d.ts | 38 | ||||
-rw-r--r-- | src/bun.js/api/bun/socket.zig | 22 | ||||
-rw-r--r-- | src/bun.js/base.zig | 99 | ||||
-rw-r--r-- | src/bun.js/bindings/ZigGlobalObject.cpp | 7 | ||||
-rw-r--r-- | src/bun.js/bindings/bindings.zig | 1 | ||||
-rw-r--r-- | src/bun.js/bindings/generated_classes.zig | 4 | ||||
-rw-r--r-- | src/bun.js/net.exports.js | 11 | ||||
-rw-r--r-- | test/bun.js/tcp-server.test.ts | 131 |
8 files changed, 292 insertions, 21 deletions
diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index c577d50ec..0de48d038 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -2623,7 +2623,7 @@ declare module "bun" { */ builder: PluginBuilder, ): void | Promise<void>; - }): ReturnType<typeof options["setup"]>; + }): ReturnType<(typeof options)["setup"]>; /** * Deactivate all plugins @@ -2761,11 +2761,26 @@ declare module "bun" { interface TCPSocket extends Socket {} interface TLSSocket extends Socket {} - interface SocketHandler<Data = unknown> { + type BinaryTypeList = { + arraybuffer: ArrayBuffer; + buffer: Buffer; + uint8array: Uint8Array; + // TODO: DataView + // dataview: DataView; + }; + type BinaryType = keyof BinaryTypeList; + + interface SocketHandler< + Data = unknown, + DataBinaryType extends BinaryType = "buffer", + > { open(socket: Socket<Data>): void | Promise<void>; close?(socket: Socket<Data>): void | Promise<void>; error?(socket: Socket<Data>, error: Error): void | Promise<void>; - data?(socket: Socket<Data>, data: BufferSource): void | Promise<void>; + data?( + socket: Socket<Data>, + data: BinaryTypeList[DataBinaryType], + ): void | Promise<void>; drain?(socket: Socket<Data>): void | Promise<void>; /** @@ -2788,6 +2803,23 @@ declare module "bun" { * to the promise rejection queue. */ connectError?(socket: Socket<Data>, error: Error): void | Promise<void>; + + /** + * Choose what `ArrayBufferView` is returned in the {@link SocketHandler.data} callback. + * + * @default "buffer" + * + * @remarks + * This lets you select the desired binary type for the `data` callback. + * It's a small performance optimization to let you avoid creating extra + * ArrayBufferView objects when possible. + * + * Bun originally defaulted to `Uint8Array` but when dealing with network + * data, it's more useful to be able to directly read from the bytes which + * `Buffer` allows. + * + */ + binaryType?: BinaryType; } interface SocketOptions<Data = unknown> { diff --git a/src/bun.js/api/bun/socket.zig b/src/bun.js/api/bun/socket.zig index 925db901d..cf482e242 100644 --- a/src/bun.js/api/bun/socket.zig +++ b/src/bun.js/api/bun/socket.zig @@ -65,6 +65,9 @@ fn normalizeHost(input: anytype) @TypeOf(input) { return input; } + +const BinaryType = JSC.BinaryType; + const Handlers = struct { onOpen: JSC.JSValue = .zero, onClose: JSC.JSValue = .zero, @@ -75,7 +78,7 @@ const Handlers = struct { onEnd: JSC.JSValue = .zero, onError: JSC.JSValue = .zero, - encoding: JSC.Node.Encoding = .utf8, + binary_type: BinaryType = .Buffer, vm: *JSC.VirtualMachine, globalObject: *JSC.JSGlobalObject, @@ -150,7 +153,6 @@ const Handlers = struct { .{ "onWritable", "drain" }, .{ "onOpen", "open" }, .{ "onClose", "close" }, - .{ "onData", "data" }, .{ "onTimeout", "timeout" }, .{ "onConnectError", "connectError" }, .{ "onEnd", "end" }, @@ -172,6 +174,18 @@ const Handlers = struct { return null; } + if (opts.getTruthy(globalObject, "binaryType")) |binary_type_value| { + if (!binary_type_value.isString()) { + exception.* = JSC.toInvalidArguments("Expected \"binaryType\" to be a string", .{}, globalObject).asObjectRef(); + return null; + } + + handlers.binary_type = BinaryType.fromJSValue(globalObject, binary_type_value) orelse { + exception.* = JSC.toInvalidArguments("Expected 'binaryType' to be 'arraybuffer', 'uint8array', 'buffer'", .{}, globalObject).asObjectRef(); + return null; + }; + } + return handlers; } @@ -1144,7 +1158,7 @@ fn NewSocket(comptime ssl: bool) type { const globalObject = handlers.globalObject; const this_value = this.getThisValue(globalObject); - const output_value = JSC.ArrayBuffer.create(globalObject, data, .Uint8Array); + const output_value = handlers.binary_type.toJS(data, globalObject); // const encoding = handlers.encoding; const result = callback.callWithThis(globalObject, this_value, &[_]JSValue{ this_value, @@ -1288,7 +1302,7 @@ fn NewSocket(comptime ssl: bool) type { } // we don't cork yet but we might later const res = this.socket.write(buffer, is_end); - log("write({d}, {any})", .{ buffer.len, is_end }); + log("write({d}, {any}) = {d}", .{ buffer.len, is_end, res }); return res; } diff --git a/src/bun.js/base.zig b/src/bun.js/base.zig index f2a305564..10a182c60 100644 --- a/src/bun.js/base.zig +++ b/src/bun.js/base.zig @@ -1715,13 +1715,18 @@ pub const ArrayBuffer = extern struct { pub fn create(globalThis: *JSC.JSGlobalObject, bytes: []const u8, comptime kind: JSC.JSValue.JSType) JSValue { JSC.markBinding(@src()); return switch (comptime kind) { - .Uint8Array => Bun__createUint8ArrayForCopy(globalThis, bytes.ptr, bytes.len), + .Uint8Array => Bun__createUint8ArrayForCopy(globalThis, bytes.ptr, bytes.len, false), .ArrayBuffer => Bun__createArrayBufferForCopy(globalThis, bytes.ptr, bytes.len), else => @compileError("Not implemented yet"), }; } - extern "C" fn Bun__createUint8ArrayForCopy(*JSC.JSGlobalObject, ptr: ?*const anyopaque, len: usize) JSValue; + pub fn createBuffer(globalThis: *JSC.JSGlobalObject, bytes: []const u8) JSValue { + JSC.markBinding(@src()); + return Bun__createUint8ArrayForCopy(globalThis, bytes.ptr, bytes.len, true); + } + + extern "C" fn Bun__createUint8ArrayForCopy(*JSC.JSGlobalObject, ptr: ?*const anyopaque, len: usize, buffer: bool) JSValue; extern "C" fn Bun__createArrayBufferForCopy(*JSC.JSGlobalObject, ptr: ?*const anyopaque, len: usize) JSValue; pub fn fromTypedArray(ctx: JSC.C.JSContextRef, value: JSC.JSValue, _: JSC.C.ExceptionRef) ArrayBuffer { @@ -3994,3 +3999,93 @@ pub const Strong = extern struct { ref.destroy(); } }; + +pub const BinaryType = enum { + Buffer, + ArrayBuffer, + Uint8Array, + Uint16Array, + Uint32Array, + Int8Array, + Int16Array, + Int32Array, + Float32Array, + Float64Array, + // DataView, + + pub fn toJSType(this: BinaryType) JSC.JSValue.JSType { + return switch (this) { + .ArrayBuffer => .ArrayBuffer, + .Buffer => .Uint8Array, + // .DataView => .DataView, + .Float32Array => .Float32Array, + .Float64Array => .Float64Array, + .Int16Array => .Int16Array, + .Int32Array => .Int32Array, + .Int8Array => .Int8Array, + .Uint16Array => .Uint16Array, + .Uint32Array => .Uint32Array, + .Uint8Array => .Uint8Array, + }; + } + + pub fn toTypedArrayType(this: BinaryType) JSC.C.JSTypedArrayType { + return this.toJSType().toC(); + } + + const Map = bun.ComptimeStringMap( + BinaryType, + .{ + .{ "ArrayBuffer", .ArrayBuffer }, + .{ "Buffer", .Buffer }, + // .{ "DataView", .DataView }, + .{ "Float32Array", .Float32Array }, + .{ "Float64Array", .Float64Array }, + .{ "Int16Array", .Int16Array }, + .{ "Int32Array", .Int32Array }, + .{ "Int8Array", .Int8Array }, + .{ "Uint16Array", .Uint16Array }, + .{ "Uint32Array", .Uint32Array }, + .{ "Uint8Array", .Uint8Array }, + .{ "arraybuffer", .ArrayBuffer }, + .{ "buffer", .Buffer }, + // .{ "dataview", .DataView }, + .{ "float32array", .Float32Array }, + .{ "float64array", .Float64Array }, + .{ "int16array", .Int16Array }, + .{ "int32array", .Int32Array }, + .{ "int8array", .Int8Array }, + .{ "nodebuffer", .Buffer }, + .{ "uint16array", .Uint16Array }, + .{ "uint32array", .Uint32Array }, + .{ "uint8array", .Uint8Array }, + }, + ); + + pub fn fromString(input: []const u8) ?BinaryType { + return Map.get(input); + } + + pub fn fromJSValue(globalThis: *JSC.JSGlobalObject, input: JSValue) ?BinaryType { + if (input.isString()) { + return Map.getWithEql(input.getZigString(globalThis), ZigString.eqlComptime); + } + + return null; + } + + /// This clones bytes + pub fn toJS(this: BinaryType, bytes: []const u8, globalThis: *JSC.JSGlobalObject) JSValue { + switch (this) { + .Buffer => return JSC.ArrayBuffer.createBuffer(globalThis, bytes), + .ArrayBuffer => return JSC.ArrayBuffer.create(globalThis, bytes, .ArrayBuffer), + .Uint8Array => return JSC.ArrayBuffer.create(globalThis, bytes, .Uint8Array), + + // These aren't documented, but they are supported + .Uint16Array, .Uint32Array, .Int8Array, .Int16Array, .Int32Array, .Float32Array, .Float64Array => { + const buffer = JSC.ArrayBuffer.create(globalThis, bytes, .ArrayBuffer); + return JSC.JSValue.c(JSC.C.JSObjectMakeTypedArrayWithArrayBuffer(globalThis, this.toTypedArrayType(), buffer.asObjectRef(), null)); + }, + } + } +}; diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp index 9f6a13b40..bfd775cde 100644 --- a/src/bun.js/bindings/ZigGlobalObject.cpp +++ b/src/bun.js/bindings/ZigGlobalObject.cpp @@ -970,10 +970,13 @@ extern "C" JSC__JSValue Bun__createArrayBufferForCopy(JSC::JSGlobalObject* globa RELEASE_AND_RETURN(scope, JSValue::encode(JSC::JSArrayBuffer::create(globalObject->vm(), globalObject->arrayBufferStructure(JSC::ArrayBufferSharingMode::Default), WTFMove(arrayBuffer)))); } -extern "C" JSC__JSValue Bun__createUint8ArrayForCopy(JSC::JSGlobalObject* globalObject, const void* ptr, size_t len) +extern "C" JSC__JSValue Bun__createUint8ArrayForCopy(JSC::JSGlobalObject* globalObject, const void* ptr, size_t len, bool isBuffer) { auto scope = DECLARE_THROW_SCOPE(globalObject->vm()); - JSC::JSUint8Array* array = JSC::JSUint8Array::createUninitialized(globalObject, globalObject->m_typedArrayUint8.get(globalObject), len); + JSC::JSUint8Array* array = JSC::JSUint8Array::createUninitialized( + globalObject, + isBuffer ? reinterpret_cast<Zig::GlobalObject*>(globalObject)->JSBufferSubclassStructure() : globalObject->m_typedArrayUint8.get(globalObject), + len); if (UNLIKELY(!array)) { JSC::throwOutOfMemoryError(globalObject, scope); diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index 23dd6188f..447c6c043 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -2582,6 +2582,7 @@ pub const JSValue = enum(JSValueReprInt) { .Float32Array => .kJSTypedArrayTypeFloat32Array, .Float64Array => .kJSTypedArrayTypeFloat64Array, .ArrayBuffer => .kJSTypedArrayTypeArrayBuffer, + // .DataView => .kJSTypedArrayTypeDataView, else => .kJSTypedArrayTypeNone, }; } diff --git a/src/bun.js/bindings/generated_classes.zig b/src/bun.js/bindings/generated_classes.zig index 9962ec7f5..18bf91b99 100644 --- a/src/bun.js/bindings/generated_classes.zig +++ b/src/bun.js/bindings/generated_classes.zig @@ -6,8 +6,8 @@ /// 1. `bun src/bun.js/scripts/generate-classes.ts` /// 2. Scan for **/*.classes.ts files in src/bun.js/src /// 3. Generate a JS wrapper for each class in: -/// Zig: generated_classes.zig -/// C++: ZigGeneratedClasses.h, ZigGeneratedClasses.cpp +/// - Zig: generated_classes.zig +/// - C++: ZigGeneratedClasses.h, ZigGeneratedClasses.cpp /// 4. For the Zig code to successfully compile: /// - Add it to generated_classes_list.zig /// - pub usingnamespace JSC.Codegen.JSMyClassName; diff --git a/src/bun.js/net.exports.js b/src/bun.js/net.exports.js index 928189dd3..b4229a271 100644 --- a/src/bun.js/net.exports.js +++ b/src/bun.js/net.exports.js @@ -86,14 +86,14 @@ export const Socket = (function (InternalSocket) { self.emit("error", error); }, - data({ data: self }, { length, buffer }) { - self.bytesRead += length; + data({ data: self }, buffer) { + self.bytesRead += buffer.length; const queue = self.#readQueue; - const ret = new Buffer(buffer); + if (queue.isEmpty()) { - if (self.push(ret)) return; + if (self.push(buffer)) return; } - queue.push(ret); + queue.push(buffer); }, drain: Socket.#Drain, end: Socket.#Close, @@ -120,6 +120,7 @@ export const Socket = (function (InternalSocket) { const self = socket.data; self.emit("timeout"); }, + binaryType: "buffer", }; static #Close(socket) { diff --git a/test/bun.js/tcp-server.test.ts b/test/bun.js/tcp-server.test.ts index 77af3c378..6ddacd662 100644 --- a/test/bun.js/tcp-server.test.ts +++ b/test/bun.js/tcp-server.test.ts @@ -1,5 +1,11 @@ -import { listen, connect, TCPSocketListener } from "bun"; -import { expect, it } from "bun:test"; +import { + listen, + connect, + TCPSocketListener, + TCPSocketOptions, + SocketHandler, +} from "bun"; +import { describe, expect, it } from "bun:test"; import * as JSC from "bun:jsc"; var decoder = new TextDecoder(); @@ -74,12 +80,13 @@ it("echo server 1 on 1", async () => { drain(socket) { reject(new Error("Unexpected backpressure")); }, - }; + } as SocketHandler<any>; var server: TCPSocketListener<any> | undefined = listen({ socket: handlers, hostname: "localhost", port: 8084, + data: { isServer: true, counter: 0, @@ -99,6 +106,124 @@ it("echo server 1 on 1", async () => { })(); }); +describe("tcp socket binaryType", () => { + var port = 8085; + const binaryType = ["arraybuffer", "uint8array", "buffer"] as const; + for (const type of binaryType) { + it(type, async () => { + // wrap it in a separate closure so the GC knows to clean it up + // the sockets & listener don't escape the closure + await (async function () { + var resolve, reject, serverResolve, serverReject; + var prom = new Promise((resolve1, reject1) => { + resolve = resolve1; + reject = reject1; + }); + var serverProm = new Promise((resolve1, reject1) => { + serverResolve = resolve1; + serverReject = reject1; + }); + + var serverData, clientData; + const handlers = { + open(socket) { + socket.data.counter = 1; + if (!socket.data?.isServer) { + clientData = socket.data; + clientData.sendQueue = ["client: Hello World! " + 0]; + if (!socket.write("client: Hello World! " + 0)) { + socket.data = { pending: "server: Hello World! " + 0 }; + } + } else { + serverData = socket.data; + serverData.sendQueue = ["server: Hello World! " + 0]; + } + + if (clientData) clientData.other = serverData; + if (serverData) serverData.other = clientData; + if (clientData) clientData.other = serverData; + if (serverData) serverData.other = clientData; + }, + data(socket, buffer) { + expect( + buffer instanceof + (type === "arraybuffer" + ? ArrayBuffer + : type === "uint8array" + ? Uint8Array + : type === "buffer" + ? Buffer + : Error), + ).toBe(true); + const msg = `${ + socket.data.isServer ? "server:" : "client:" + } Hello World! ${socket.data.counter++}`; + socket.data.sendQueue.push(msg); + + expect(decoder.decode(buffer)).toBe( + socket.data.other.sendQueue.pop(), + ); + + if (socket.data.counter > 10) { + if (!socket.data.finished) { + socket.data.finished = true; + if (socket.data.isServer) { + setTimeout(() => { + serverResolve(); + socket.end(); + }, 1); + } else { + setTimeout(() => { + resolve(); + socket.end(); + }, 1); + } + } + } + + if (!socket.write(msg)) { + socket.data.pending = msg; + return; + } + }, + error(socket, error) { + reject(error); + }, + drain(socket) { + reject(new Error("Unexpected backpressure")); + }, + + binaryType: type, + } as SocketHandler<any>; + + var server: TCPSocketListener<any> | undefined = listen({ + socket: handlers, + hostname: "localhost", + port, + data: { + isServer: true, + counter: 0, + }, + }); + + const clientProm = connect({ + socket: handlers, + hostname: "localhost", + port, + data: { + counter: 0, + }, + }); + port++; + + await Promise.all([prom, clientProm, serverProm]); + server.stop(true); + server = serverData = clientData = undefined; + })(); + }); + } +}); + it("should not leak memory", () => { // Tell the garbage collector for sure that we're done with the sockets Bun.gc(true); |