@sproutboat/runtime 0.33.0 → 0.34.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,133 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join, resolve } from "node:path";
5
+ import { ensurePorffor } from "../../toolchain/src/acquire";
6
+ import { preludePath, TRANSPORT_MARKER, transportPath, wrapNativeFetchHandler } from "../src/wrap";
7
+
8
+ // A promise that rejects with nobody waiting on it (a binding call that failed and was never
9
+ // awaited, say a D1 run() that broke a foreign key) must not take the process down, whatever
10
+ // the compatibility date. As on Workers it is logged, an "unhandledrejection" listener hears
11
+ // it, and the next request is served.
12
+ const handler = `addEventListener("unhandledrejection", (event) => {
13
+ globalThis.__heard = (globalThis.__heard || []).concat(String(event.reason && event.reason.message));
14
+ });
15
+ export default {async fetch(request) {
16
+ const path = new URL(request.url).pathname;
17
+ if (path === "/reject") {
18
+ try {
19
+ Promise.reject(new Error("D1_ERROR: FOREIGN KEY constraint failed"));
20
+ } catch (error) {
21
+ return new Response("caught", { status: 500 });
22
+ }
23
+ return new Response("fired");
24
+ }
25
+ if (path === "/promise") {
26
+ try {
27
+ new Promise(function (resolve, reject) { reject(new Error("kv failed")); });
28
+ } catch (error) {
29
+ return new Response("caught", { status: 500 });
30
+ }
31
+ return new Response("fired");
32
+ }
33
+ if (path === "/async") {
34
+ async function failNow() { throw new Error("async function failed"); }
35
+ failNow();
36
+ return new Response("fired");
37
+ }
38
+ if (path === "/heard") return new Response(JSON.stringify(globalThis.__heard || []));
39
+ return new Response("pong");
40
+ }};`;
41
+
42
+ function freePort(): number {
43
+ const listener = Bun.listen({ hostname: "127.0.0.1", port: 0, socket: { data() {} } });
44
+ const port = listener.port;
45
+ listener.stop(true);
46
+ return port;
47
+ }
48
+
49
+ for (const date of ["2026-09-28", "2026-10-24"]) {
50
+ const workdir = await mkdtemp(join(tmpdir(), "sb-native-unhandled-"));
51
+ let server: ReturnType<typeof Bun.spawn> | undefined;
52
+ try {
53
+ const porffor = await ensurePorffor();
54
+ const node = Bun.which("node");
55
+ if (!node) throw new Error("native unhandled rejection test needs node on PATH");
56
+ const [core, broker] = await Promise.all([
57
+ readFile(preludePath, "utf8"),
58
+ readFile(transportPath("broker"), "utf8"),
59
+ ]);
60
+ assert(core.includes(TRANSPORT_MARKER), "runtime prelude is missing its transport marker");
61
+ const generated = join(workdir, "unhandled.generated.js");
62
+ const binary = join(workdir, "unhandled-native");
63
+ await writeFile(
64
+ generated,
65
+ wrapNativeFetchHandler(handler, core.replace(TRANSPORT_MARKER, broker), undefined, undefined, undefined, date),
66
+ );
67
+ const compile = Bun.spawn(
68
+ [node, resolve(porffor, "runtime/index.js"), "native", generated, "-o", binary, "-s", "-O0"],
69
+ {
70
+ cwd: workdir,
71
+ env: process.env,
72
+ stdout: "pipe",
73
+ stderr: "pipe",
74
+ },
75
+ );
76
+ const [exitCode, stdout, stderr] = await Promise.all([
77
+ compile.exited,
78
+ new Response(compile.stdout).text(),
79
+ new Response(compile.stderr).text(),
80
+ ]);
81
+ assert.equal(exitCode, 0, `Porffor native compile failed:\n${stderr}\n${stdout}`);
82
+
83
+ const port = freePort();
84
+ server = Bun.spawn([binary], {
85
+ cwd: workdir,
86
+ env: { ...process.env, PORT: String(port), SB_BROKER_TOKEN: "native-test-token" },
87
+ stdout: "ignore",
88
+ stderr: "pipe",
89
+ });
90
+ let ready = false;
91
+ for (let attempt = 0; attempt < 50 && !ready; attempt++) {
92
+ try {
93
+ await fetch(`http://127.0.0.1:${port}/`, { signal: AbortSignal.timeout(500) });
94
+ ready = true;
95
+ } catch {
96
+ if (server.exitCode !== null) break;
97
+ await Bun.sleep(100);
98
+ }
99
+ }
100
+ assert(ready, `compiled handler did not start (${date})`);
101
+ const get = async (path: string) => (await fetch(`http://127.0.0.1:${port}${path}`)).text();
102
+
103
+ assert.equal(await get("/reject"), "fired");
104
+ assert.equal(await get("/pong"), "pong", `the request after an unhandled rejection failed (${date})`);
105
+ assert.equal(await get("/promise"), "fired");
106
+ assert.equal(await get("/async"), "fired");
107
+ await Bun.sleep(300);
108
+ assert.equal(await get("/pong"), "pong", `the request after a rejected promise failed (${date})`);
109
+ assert.equal(server.exitCode, null, `the server exited on an unhandled rejection (${date})`);
110
+ assert.equal(await get("/pong"), "pong", `the request after an unawaited async function threw failed (${date})`);
111
+ const heard = JSON.parse(await get("/heard"));
112
+ assert(
113
+ heard.some((message: string) => message.includes("FOREIGN KEY")),
114
+ `no unhandledrejection event for the rejected promise (${date}): ${JSON.stringify(heard)}`,
115
+ );
116
+ assert(
117
+ heard.some((message: string) => message.includes("async function failed")),
118
+ `no unhandledrejection event for the async function (${date}): ${JSON.stringify(heard)}`,
119
+ );
120
+ server.kill(9);
121
+ await server.exited;
122
+ const log = await new Response(server.stderr).text();
123
+ assert(log.includes("Uncaught (in promise)"), `the rejection was not logged (${date}): ${log}`);
124
+ server = undefined;
125
+ console.log(`Native unhandled rejections are reported, not fatal (${date})`);
126
+ } finally {
127
+ if (server) {
128
+ server.kill(9);
129
+ await server.exited;
130
+ }
131
+ await rm(workdir, { recursive: true, force: true });
132
+ }
133
+ }
@@ -0,0 +1,244 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+
6
+ // Outbound WebSockets, from compatibility date 2026-11-01: new WebSocket(url) and fetch() with
7
+ // Upgrade: websocket, on the embedded transport (standalone sprouts, sproutd). An upstream
8
+ // server in this process (Bun) answers them; the handler's answers are what workerd gives for
9
+ // the same code.
10
+ interface UpstreamData {
11
+ readonly path: string;
12
+ }
13
+ const upstream = Bun.serve<UpstreamData>({
14
+ port: 0,
15
+ hostname: "127.0.0.1",
16
+ fetch(request, server) {
17
+ const path = new URL(request.url).pathname;
18
+ if (path === "/refuse") return new Response("no sockets", { status: 426, headers: { "x-why": "test" } });
19
+ const ok = server.upgrade(request, {
20
+ data: { path },
21
+ headers: path === "/proto" ? { "sec-websocket-protocol": "chat" } : undefined,
22
+ });
23
+ return ok ? undefined : new Response("expected websocket", { status: 426 });
24
+ },
25
+ websocket: {
26
+ open(ws) {
27
+ if (ws.data.path === "/hello") ws.send("hello from upstream");
28
+ if (ws.data.path === "/bye") ws.close(4003, "upstream bye");
29
+ },
30
+ message(ws, message) {
31
+ if (message === "close-me") ws.close(4004, "asked");
32
+ else if (message instanceof Buffer) ws.send(message);
33
+ else ws.send(`echo:${message}`);
34
+ },
35
+ maxPayloadLength: 64 * 1024 * 1024,
36
+ },
37
+ });
38
+
39
+ const handler = `export default {async fetch(request) {
40
+ const url = new URL(request.url);
41
+ const path = url.pathname;
42
+ const up = "ws://127.0.0.1:${upstream.port}";
43
+ const tick = (ms) => new Promise((r) => setTimeout(r, ms));
44
+ const waitFor = async (cond, ms) => { const end = Date.now() + ms; while (!cond() && Date.now() < end) await tick(10); return cond(); };
45
+ if (path === "/ctor") {
46
+ const log = [];
47
+ const ws = new WebSocket(up + "/proto", ["other", "chat"]);
48
+ log.push("state " + ws.readyState + " " + ws.url.replace(/:\\d+/, ":PORT"));
49
+ ws.addEventListener("open", () => { log.push("open " + ws.protocol + " " + ws.readyState); ws.send("hi"); ws.send(new Uint8Array([1, 2, 3])); });
50
+ ws.binaryType = "arraybuffer";
51
+ ws.addEventListener("message", (e) => {
52
+ log.push(typeof e.data === "string" ? "m:" + e.data : "b:" + Array.from(new Uint8Array(e.data)).join(","));
53
+ if (log.filter((l) => l[0] === "m" || l[0] === "b").length === 2) ws.close(1000, "done");
54
+ });
55
+ ws.addEventListener("close", (e) => log.push("close " + e.code + " " + e.reason + " " + e.wasClean + " " + ws.readyState));
56
+ await waitFor(() => log.some((l) => l.indexOf("close") === 0), 5000);
57
+ return Response.json(log);
58
+ }
59
+ if (path === "/ctor-early") {
60
+ // send() while CONNECTING throws; a greeting from the server arrives; the server's close reaches us.
61
+ const log = [];
62
+ const ws = new WebSocket(up + "/hello");
63
+ try { ws.send("too early"); } catch (e) { log.push(e.name); }
64
+ ws.addEventListener("message", (e) => { log.push(e.data); ws.send("close-me"); });
65
+ ws.addEventListener("close", (e) => log.push("close " + e.code + " " + e.reason + " " + e.wasClean));
66
+ await waitFor(() => log.some((l) => l.indexOf("close") === 0), 5000);
67
+ return Response.json(log);
68
+ }
69
+ if (path === "/serverclose") {
70
+ const log = [];
71
+ const ws = new WebSocket(up + "/bye");
72
+ ws.addEventListener("close", (e) => log.push("close " + e.code + " " + e.reason + " " + e.wasClean));
73
+ await waitFor(() => log.length > 0, 5000);
74
+ return Response.json(log);
75
+ }
76
+ if (path === "/big") {
77
+ const ws = new WebSocket(up + "/echo");
78
+ const big = "y".repeat(3 * 1024 * 1024);
79
+ let got = 0;
80
+ ws.addEventListener("open", () => ws.send(big));
81
+ ws.addEventListener("message", (e) => { got = e.data.length; ws.close(1000); });
82
+ await waitFor(() => got > 0, 10000);
83
+ return Response.json({ got, sent: big.length });
84
+ }
85
+ if (path === "/fetch") {
86
+ const res = await fetch("http://127.0.0.1:${upstream.port}/echo", { headers: { Upgrade: "websocket" } });
87
+ const ws = res.webSocket;
88
+ const log = [res.status, ws !== null, res.headers.get("upgrade")];
89
+ ws.accept();
90
+ ws.addEventListener("message", (e) => log.push(e.data));
91
+ ws.send("one");
92
+ ws.send("two");
93
+ await waitFor(() => log.length >= 5, 5000);
94
+ ws.close(1000, "bye");
95
+ await tick(100);
96
+ log.push("state " + ws.readyState);
97
+ return Response.json(log);
98
+ }
99
+ if (path === "/refused") {
100
+ const res = await fetch("http://127.0.0.1:${upstream.port}/refuse", { headers: { Upgrade: "websocket" } });
101
+ return Response.json([res.status, res.webSocket, res.headers.get("x-why"), await res.text()]);
102
+ }
103
+ if (path === "/proxy") {
104
+ // The handler returns the upstream's upgrade response: the client's socket is relayed to it.
105
+ return fetch("http://127.0.0.1:${upstream.port}/proto", request);
106
+ }
107
+ if (path === "/badurl") {
108
+ const out = [];
109
+ try { new WebSocket("ftp://example.com/"); } catch (e) { out.push(e.name); }
110
+ try { new WebSocket("not a url"); } catch (e) { out.push(e.name); }
111
+ try { new WebSocket("ws://example.com/#frag"); } catch (e) { out.push(e.name); }
112
+ try { new WebSocket("ws://example.com/", ["a", "a"]); } catch (e) { out.push(e.name); }
113
+ return Response.json(out);
114
+ }
115
+ if (path === "/unreachable") {
116
+ const log = [];
117
+ const ws = new WebSocket("ws://127.0.0.1:1/nothing");
118
+ ws.addEventListener("error", () => log.push("error"));
119
+ ws.addEventListener("close", (e) => log.push("close " + e.code + " " + e.wasClean));
120
+ await waitFor(() => log.length >= 2, 8000);
121
+ return Response.json(log);
122
+ }
123
+ return new Response("no route " + path, { status: 404 });
124
+ }};`;
125
+
126
+ const cli = process.env.SB_CLI;
127
+ if (!cli) {
128
+ console.log("skipped: set SB_CLI to a sproutboat-cli checkout linked to this checkout (it builds the sprouts)");
129
+ process.exit(0);
130
+ }
131
+
132
+ function freePort(): number {
133
+ const listener = Bun.listen({ hostname: "127.0.0.1", port: 0, socket: { data() {} } });
134
+ const port = listener.port;
135
+ listener.stop(true);
136
+ return port;
137
+ }
138
+
139
+ const workdir = mkdtempSync(join(tmpdir(), "sb-native-websockets-out-"));
140
+ const servers: ReturnType<typeof Bun.spawn>[] = [];
141
+ // A standalone sprout built by the CLI (the embedded transport) at each compatibility date.
142
+ const start = async (date: string) => {
143
+ const slug = `wsout-${date}`;
144
+ const project = join(workdir, slug);
145
+ mkdirSync(join(project, "src"), { recursive: true });
146
+ writeFileSync(join(project, "src/index.js"), handler);
147
+ writeFileSync(
148
+ join(project, "sproutboat.jsonc"),
149
+ JSON.stringify({ name: slug, main: "src/index.js", compatibility_date: date }),
150
+ );
151
+ const built = Bun.spawnSync(["bun", join(cli, "src/main.ts"), "build", "--standalone", "--target", "host"], {
152
+ cwd: project,
153
+ stdout: "inherit",
154
+ stderr: "inherit",
155
+ });
156
+ assert.equal(built.exitCode, 0, "the CLI could not build the sprout");
157
+ const port = freePort();
158
+ const server = Bun.spawn([join(project, "dist", slug)], {
159
+ cwd: project,
160
+ env: { ...process.env, PORT: String(port), SB_EGRESS_ALLOW: "127.0.0.1", SB_DATA_DIR: join(project, "data") },
161
+ stdout: "ignore",
162
+ stderr: "inherit",
163
+ });
164
+ servers.push(server);
165
+ const base = `http://127.0.0.1:${port}`;
166
+ for (let attempt = 0; attempt < 100; attempt++) {
167
+ try {
168
+ await fetch(`${base}/badurl`, { signal: AbortSignal.timeout(500) });
169
+ return base;
170
+ } catch {
171
+ if (server.exitCode !== null) break;
172
+ await Bun.sleep(100);
173
+ }
174
+ }
175
+ throw new Error("compiled handler did not start");
176
+ };
177
+
178
+ try {
179
+ const now = await start("2026-11-01");
180
+ const get = async (path: string) => (await fetch(`${now}${path}`)).json();
181
+
182
+ assert.deepEqual(await get("/badurl"), ["SyntaxError", "SyntaxError", "SyntaxError", "SyntaxError"]);
183
+ assert.deepEqual(await get("/ctor"), [
184
+ "state 0 ws://127.0.0.1:PORT/proto",
185
+ "open chat 1",
186
+ "m:echo:hi",
187
+ "b:1,2,3",
188
+ "close 1000 done true 3",
189
+ ]);
190
+ assert.deepEqual(await get("/ctor-early"), ["InvalidStateError", "hello from upstream", "close 4004 asked true"]);
191
+ assert.deepEqual(await get("/serverclose"), ["close 4003 upstream bye true"]);
192
+ assert.deepEqual(await get("/big"), { got: 3 * 1024 * 1024 + 5, sent: 3 * 1024 * 1024 });
193
+ assert.deepEqual(await get("/fetch"), [101, true, "websocket", "echo:one", "echo:two", "state 3"]);
194
+ assert.deepEqual(await get("/refused"), [426, null, "test", "no sockets"]);
195
+ assert.deepEqual(await get("/unreachable"), ["error", "close 1006 false"]);
196
+ console.log("new WebSocket(url) and fetch() upgrades connect, exchange text and bytes, and close with their codes");
197
+
198
+ // The handler returns the upstream's upgrade response: a client of the sprout talks to the upstream.
199
+ const ws = new WebSocket(`${now.replace("http:", "ws:")}/proxy`, "chat");
200
+ ws.binaryType = "arraybuffer";
201
+ const events: string[] = [];
202
+ await new Promise<void>((done, fail) => {
203
+ const timer = setTimeout(() => fail(new Error("proxy timed out: " + events.join("|"))), 10000);
204
+ ws.addEventListener("open", () => ws.send("via proxy"));
205
+ ws.addEventListener("message", (e) => {
206
+ events.push(String(e.data));
207
+ ws.close(1000, "done");
208
+ });
209
+ ws.addEventListener("close", (e) => {
210
+ events.push(`close ${e.code}`);
211
+ clearTimeout(timer);
212
+ done();
213
+ });
214
+ });
215
+ assert.deepEqual(events, ["echo:via proxy", "close 1000"]);
216
+ console.log("a handler that returns an upstream upgrade relays the client's socket to it");
217
+
218
+ // Before the date the CLI refuses `new WebSocket(url)` at build time.
219
+ const old = join(workdir, "old");
220
+ mkdirSync(join(old, "src"), { recursive: true });
221
+ writeFileSync(join(old, "src/index.js"), handler);
222
+ writeFileSync(
223
+ join(old, "sproutboat.jsonc"),
224
+ JSON.stringify({ name: "wsold", main: "src/index.js", compatibility_date: "2026-10-30" }),
225
+ );
226
+ const refused = Bun.spawnSync(["bun", join(cli, "src/main.ts"), "check"], {
227
+ cwd: old,
228
+ stdout: "pipe",
229
+ stderr: "pipe",
230
+ });
231
+ assert.notEqual(refused.exitCode, 0);
232
+ assert.match(
233
+ refused.stdout.toString() + refused.stderr.toString(),
234
+ /WebSocket\(\) needs compatibility_date 2026-11-01/,
235
+ );
236
+ console.log("before 2026-11-01 the build refuses new WebSocket(url)");
237
+ } finally {
238
+ for (const server of servers) {
239
+ server.kill(9);
240
+ await server.exited;
241
+ }
242
+ upstream.stop(true);
243
+ rmSync(workdir, { recursive: true, force: true });
244
+ }
@@ -9,7 +9,8 @@ import { preludePath, TRANSPORT_MARKER, transportPath, wrapNativeFetchHandler }
9
9
  // the worker is plain JavaScript and works on every host; a 101 response needs a host that
10
10
  // carries client sockets (sproutd), so a standalone sprout answers it with a 501. Before the
11
11
  // date nothing is defined. The answers below are workerd's (1.20260730.1) for the same code.
12
- const handler = `export default {async fetch(request) {
12
+ const handler = `const closes = [];
13
+ export default {async fetch(request) {
13
14
  const path = new URL(request.url).pathname;
14
15
  const tick = () => new Promise((r) => setTimeout(r, 10));
15
16
  if (path === "/shape") return Response.json([typeof WebSocketPair, typeof WebSocket, typeof WebSocketRequestResponsePair, new Response("x").webSocket]);
@@ -69,6 +70,20 @@ const handler = `export default {async fetch(request) {
69
70
  const pair = new WebSocketPair();
70
71
  try { pair[0].send("x"); return new Response("sent"); } catch (e) { return new Response(e.name + ": " + e.message); }
71
72
  }
73
+ if (path === "/echo" || path === "/hello" || path === "/proto" || path === "/serverclose") {
74
+ if (request.headers.get("Upgrade") !== "websocket") return new Response("expected websocket", { status: 426 });
75
+ const [client, server] = Object.values(new WebSocketPair());
76
+ server.accept();
77
+ if (path === "/hello") server.send("hello");
78
+ if (path === "/serverclose") server.close(4002, "server says bye");
79
+ if (path === "/echo") {
80
+ server.binaryType = "arraybuffer";
81
+ server.addEventListener("message", (e) => server.send(typeof e.data === "string" ? "echo:" + e.data : e.data));
82
+ server.addEventListener("close", (e) => closes.push([e.code, e.reason, e.wasClean]));
83
+ }
84
+ return new Response(null, { status: 101, webSocket: client, headers: path === "/proto" ? { "Sec-WebSocket-Protocol": "chat" } : {} });
85
+ }
86
+ if (path === "/closes") return Response.json(closes);
72
87
  if (path === "/upgrade") {
73
88
  const [client, server] = Object.values(new WebSocketPair());
74
89
  server.accept();
@@ -136,7 +151,7 @@ try {
136
151
  }
137
152
  throw new Error("compiled handler did not start");
138
153
  };
139
- const [now, before] = await Promise.all([start("2026-10-30"), start("2026-10-29")]);
154
+ const [now, before, hosted] = await Promise.all([start("2026-10-30"), start("2026-10-29"), start("2026-11-01")]);
140
155
 
141
156
  assert.deepEqual(await (await fetch(`${now}/shape`)).json(), ["function", "function", "function", null]);
142
157
  assert.deepEqual(await (await fetch(`${before}/shape`)).json(), ["undefined", "undefined", "undefined", null]);
@@ -168,7 +183,75 @@ try {
168
183
  assert.equal(await upgrade.text(), "WebSockets are not supported by this host yet");
169
184
  assert.equal(await (await fetch(`${now}/bad101`)).text(), "RangeError");
170
185
  assert.equal(await (await fetch(`${before}/bad101`)).text(), "RangeError");
171
- console.log("a standalone sprout answers a 101 response with 501; status 101 needs a WebSocket");
186
+ console.log("a standalone sprout answers a 101 response with 501 before 2026-11-01; status 101 needs a WebSocket");
187
+
188
+ // From 2026-11-01 the server carries the client end: a real WebSocket.
189
+ const wsBase = hosted.replace("http:", "ws:");
190
+ const open = (url: string, protocols?: string | string[]) =>
191
+ new Promise<WebSocket>((ok, fail) => {
192
+ const ws = protocols ? new WebSocket(url, protocols) : new WebSocket(url);
193
+ ws.binaryType = "arraybuffer";
194
+ ws.addEventListener("open", () => ok(ws), { once: true });
195
+ ws.addEventListener("error", () => fail(new Error("WebSocket error for " + url)), { once: true });
196
+ });
197
+ const nextMessage = (ws: WebSocket) =>
198
+ new Promise<MessageEvent["data"]>((ok, fail) => {
199
+ const t = setTimeout(() => fail(new Error("timed out waiting for a message")), 5000);
200
+ ws.addEventListener(
201
+ "message",
202
+ (e) => {
203
+ clearTimeout(t);
204
+ ok(e.data);
205
+ },
206
+ { once: true },
207
+ );
208
+ });
209
+ const closedWith = (ws: WebSocket) =>
210
+ new Promise<[number, string]>((ok) => ws.addEventListener("close", (e) => ok([e.code, e.reason]), { once: true }));
211
+
212
+ const echo = await open(`${wsBase}/echo`);
213
+ echo.send("hi");
214
+ assert.equal(await nextMessage(echo), "echo:hi");
215
+ echo.send(new Uint8Array([0, 1, 2, 255]));
216
+ assert.deepEqual(Array.from(new Uint8Array(await nextMessage(echo))), [0, 1, 2, 255]);
217
+ for (let i = 0; i < 200; i++) echo.send(String(i));
218
+ const many: string[] = [];
219
+ await new Promise<void>((done) =>
220
+ echo.addEventListener("message", (e) => {
221
+ many.push(String(e.data));
222
+ if (many.length === 200) done();
223
+ }),
224
+ );
225
+ assert.deepEqual(
226
+ many,
227
+ Array.from({ length: 200 }, (_, i) => "echo:" + i),
228
+ );
229
+ const big = "x".repeat(3 * 1024 * 1024);
230
+ echo.send(big);
231
+ const bigEcho = await nextMessage(echo);
232
+ assert.equal(bigEcho.length, big.length + 5);
233
+ const gone = closedWith(echo);
234
+ echo.close(4001, "bye");
235
+ assert.deepEqual(await gone, [4001, "bye"]);
236
+ await Bun.sleep(100);
237
+ assert.deepEqual(await (await fetch(`${hosted}/closes`)).json(), [[4001, "bye", true]]);
238
+ console.log("a client socket echoes text, bytes, 200 queued messages and a 3 MiB message, and closes with its code");
239
+
240
+ const hello = await open(`${wsBase}/hello`);
241
+ assert.equal(await nextMessage(hello), "hello");
242
+ hello.close();
243
+ const proto = await open(`${wsBase}/proto`, ["other", "chat"]);
244
+ assert.equal(proto.protocol, "chat");
245
+ proto.close();
246
+ const bye = await open(`${wsBase}/serverclose`);
247
+ assert.deepEqual(await closedWith(bye), [4002, "server says bye"]);
248
+ console.log("a message sent before the 101 arrives first, the app picks the protocol, and the app can close");
249
+
250
+ assert.equal((await fetch(`${hosted}/echo`)).status, 426);
251
+ const plain = await fetch(`${hosted}/upgrade`);
252
+ assert.equal(plain.status, 500);
253
+ assert.equal(await plain.text(), "WebSocket response to a request that is not an upgrade");
254
+ console.log("a plain request is not upgraded, and a 101 to one is a 500");
172
255
  } finally {
173
256
  for (const server of servers) {
174
257
  server.kill(9);