Files
bun.sh/test/js/bun/net/tcp-server.test.ts
taylor.fish f14f3b03bb Add new bindings generator; port SSLConfig (#23169)
Add a new generator for JS → Zig bindings. The bulk of the conversion is
done in C++, after which the data is transformed into an FFI-safe
representation, passed to Zig, and then finally transformed into
idiomatic Zig types.

In its current form, the new bindings generator supports:

* Signed and unsigned integers
* Floats (plus a “finite” variant that disallows NaN and infinities)
* Strings
* ArrayBuffer (accepts ArrayBuffer, TypedArray, or DataView)
* Blob
* Optional types
* Nullable types (allows null, whereas Optional only allows undefined)
* Arrays
* User-defined string enumerations
* User-defined unions (fields can optionally be named to provide a
better experience in Zig)
* Null and undefined, for use in unions (can more efficiently represent
optional/nullable unions than wrapping a union in an optional)
* User-defined dictionaries (arbitrary key-value pairs; expects a JS
object and parses it into a struct)
* Default values for dictionary members
* Alternative names for dictionary members (e.g., to support both
`serverName` and `servername` without taking up twice the space)
* Descriptive error messages
* Automatic `fromJS` functions in Zig for dictionaries
* Automatic `deinit` functions for the generated Zig types

Although this bindings generator has many features not present in
`bindgen.ts`, it does not yet implement all of `bindgen.ts`'s
functionality, so for the time being, it has been named `bindgenv2`, and
its configuration is specified in `.bindv2.ts` files. Once all
`bindgen.ts`'s functionality has been incorporated, it will be renamed.

This PR ports `SSLConfig` to use the new bindings generator; see
`SSLConfig.bindv2.ts`.

(For internal tracking: fixes STAB-1319, STAB-1322, STAB-1323,
STAB-1324)

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Alistair Smith <hi@alistair.sh>
2025-10-03 17:10:28 -07:00

304 lines
8.5 KiB
TypeScript

import { connect, listen, SocketHandler, TCPSocketListener } from "bun";
import { describe, expect, it } from "bun:test";
import { expectMaxObjectTypeCount, isWindows } from "harness";
type Resolve = (value?: unknown) => void;
type Reject = (reason?: any) => void;
const decoder = new TextDecoder();
it("remoteAddress works", async () => {
var resolve: Resolve, reject: Reject;
var remaining = 2;
var prom = new Promise<void>((resolve1, reject1) => {
resolve = () => {
if (--remaining === 0) resolve1();
};
reject = reject1;
});
using server = Bun.listen({
socket: {
open(ws) {
try {
expect(ws.remoteAddress).toBe("127.0.0.1");
resolve();
} catch (e) {
reject(e);
return;
}
},
close() {},
data() {},
},
port: 0,
hostname: "127.0.0.1",
});
await Bun.connect({
socket: {
open(ws) {
try {
// windows returns the ipv6 address
expect(ws.remoteAddress).toMatch(/127.0.0.1/);
resolve();
} catch (e) {
reject(e);
return;
} finally {
ws.end();
}
},
data() {},
close() {},
},
hostname: server.hostname,
port: server.port,
});
await prom;
});
it("should not allow invalid tls option", () => {
[1, "string", Symbol("symbol")].forEach(value => {
expect(() => {
// @ts-ignore
using server = Bun.listen({
socket: {
open(ws) {},
close() {},
data() {},
},
port: 0,
hostname: "localhost",
tls: value,
});
}).toThrow("TLSOptions must be an object");
});
});
it("should allow using false, null or undefined tls option", () => {
[false, null, undefined].forEach(value => {
expect(() => {
// @ts-ignore
using server = Bun.listen({
socket: {
open(ws) {},
close() {},
data() {},
},
port: 0,
hostname: "localhost",
tls: value,
});
}).not.toThrow("TLSOptions must be an object");
});
});
it("echo server 1 on 1", 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 () {
let resolve: Resolve, reject: Reject, serverResolve: Resolve, serverReject: Reject;
const prom = new Promise((resolve1, reject1) => {
resolve = resolve1;
reject = reject1;
});
const serverProm = new Promise((resolve1, reject1) => {
serverResolve = resolve1;
serverReject = reject1;
});
let serverData: any, clientData: any;
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) {
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"));
},
} as SocketHandler<any>;
using server: TCPSocketListener<any> | undefined = listen({
socket: handlers,
hostname: "localhost",
port: 0,
data: {
isServer: true,
counter: 0,
},
});
const clientProm = connect({
socket: handlers,
hostname: "localhost",
port: server.port,
data: {
counter: 0,
},
});
await Promise.all([prom, clientProm, serverProm]);
})();
});
describe("tcp socket binaryType", () => {
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 () {
let resolve: Resolve, reject: Reject, serverResolve: Resolve, serverReject: Reject;
const prom = new Promise((resolve1, reject1) => {
resolve = resolve1;
reject = reject1;
});
const serverProm = new Promise((resolve1, reject1) => {
serverResolve = resolve1;
serverReject = reject1;
});
let serverData: any, clientData: any;
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>;
using server: TCPSocketListener<any> | undefined = listen({
socket: handlers,
hostname: "localhost",
port: 0,
data: {
isServer: true,
counter: 0,
},
});
const clientProm = connect({
socket: handlers,
hostname: "localhost",
port: server.port,
data: {
counter: 0,
},
});
await Promise.all([prom, clientProm, serverProm]);
})();
});
}
});
it("should not leak memory", async () => {
// assert we don't leak the sockets
// we expect 1 or 2 because that's the prototype / structure
await expectMaxObjectTypeCount(expect, "Listener", 2);
await expectMaxObjectTypeCount(expect, "TCPSocket", isWindows ? 3 : 2);
});