1import assert from 'node:assert/strict';
2import { readFile } from 'node:fs/promises';
3import test from 'node:test';
4
5const opened = [];
6class WebSocket {
7 static OPEN = 1;
8 constructor() {
9 this.readyState = WebSocket.OPEN;
10 this.bufferedAmount = 0;
11 this.sent = [];
12 this.closes = 0;
13 opened.push(this);
14 }
15 send(bytes) {
16 if (this.failure) throw this.failure;
17 this.sent.push(bytes.slice());
18 this.bufferedAmount += bytes.length;
19 }
20 close() {
21 this.closes++;
22 this.readyState = 3;
23 }
24}
25globalThis.WebSocket = WebSocket;
26const source = await readFile(new URL('../../crates/notebook/src/live/web.js', import.meta.url), 'utf8');
27const { liveConnect, liveSend, liveClose } = await import(
28 `data:text/javascript;base64,${Buffer.from(source).toString('base64')}`
29);
30
31for (const accepted of [0, 1]) {
32 test(`backpressure after ${accepted} chunks retires the socket before notifying`, () => {
33 let disconnected = 0;
34 const id = liveConnect('ws://disposable.invalid', () => {}, () => {
35 disconnected++;
36 assert.throws(() => liveSend(id, new Uint8Array(1)), /Relay disconnected/);
37 });
38 const socket = opened.at(-1);
39 const onclose = socket.onclose;
40 const chunk = new Uint8Array(64 << 10);
41 socket.bufferedAmount = accepted ? 1048576 - chunk.length + 1 : 1048577;
42 if (accepted) liveSend(id, chunk);
43 assert.throws(() => liveSend(id, chunk), /Relay cannot keep up/);
44 assert.equal(socket.sent.length, accepted);
45 assert.equal(socket.closes, 1);
46 assert.equal(disconnected, 1);
47 assert.equal(socket.onmessage, null);
48 assert.equal(socket.onclose, null);
49 onclose();
50 liveClose(id);
51 assert.equal(socket.closes, 1);
52 assert.equal(disconnected, 1);
53
54 const replacement = liveConnect('ws://disposable.invalid', () => {}, () => disconnected++);
55 liveSend(replacement, chunk);
56 assert.throws(() => liveSend(id, chunk), /Relay disconnected/);
57 onclose();
58 assert.equal(opened.at(-1).sent.length, 1);
59 assert.equal(opened.at(-1).closes, 0);
60 assert.equal(disconnected, 1);
61 liveClose(replacement);
62 });
63}
64
65test('a WebSocket send exception retires the socket and preserves the error', () => {
66 let disconnected = 0;
67 const id = liveConnect('ws://disposable.invalid', () => {}, () => disconnected++);
68 const socket = opened.at(-1);
69 socket.failure = new Error('Transport failed');
70 assert.throws(() => liveSend(id, new Uint8Array(1)), error => error === socket.failure);
71 assert.equal(socket.sent.length, 0);
72 assert.equal(socket.closes, 1);
73 assert.equal(disconnected, 1);
74});
75
76test('a non-open socket is retired, while deliberate close never reconnects', () => {
77 let disconnected = 0;
78 const id = liveConnect('ws://disposable.invalid', () => {}, () => disconnected++);
79 const socket = opened.at(-1);
80 socket.readyState = 2;
81 assert.throws(() => liveSend(id, new Uint8Array(1)), /Relay disconnected/);
82 assert.equal(socket.closes, 1);
83 assert.equal(disconnected, 1);
84 const next = liveConnect('ws://disposable.invalid', () => {}, () => disconnected++);
85 const onclose = opened.at(-1).onclose;
86 liveClose(next);
87 onclose();
88 assert.equal(opened.at(-1).closes, 1);
89 assert.equal(disconnected, 1);
90});