aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> 2023-01-29 22:33:37 -0800
committerGravatar Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> 2023-01-29 22:33:37 -0800
commit703bee976b9961a3cccc96c371c833546f3505d9 (patch)
treece531762755f626837c593310e4264be1b1f9325
parenteb5105aa09767da7d015299f956ed0fdfdffb386 (diff)
downloadbun-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.ts38
-rw-r--r--src/bun.js/api/bun/socket.zig22
-rw-r--r--src/bun.js/base.zig99
-rw-r--r--src/bun.js/bindings/ZigGlobalObject.cpp7
-rw-r--r--src/bun.js/bindings/bindings.zig1
-rw-r--r--src/bun.js/bindings/generated_classes.zig4
-rw-r--r--src/bun.js/net.exports.js11
-rw-r--r--test/bun.js/tcp-server.test.ts131
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);