A runtime agnostic WebSocket server. Tested with node, deno, bun, tjs, and chrome, see https://gist.github.com/guest271314/d330c7cea513f12ef7bf523c56431453.
Do I want feedback about any or all facets of the code?
any aspect of the code posted is fair game for feedback and criticism.
// JavaScript runtime agnostic WebSocket server
//
// Fork of https://gist.github.com/d0ruk/3921918937e234988dfaccfdee781bd3
//
// The Definitive Guide to HTML5 WebSocket by Vanessa Wang, Frank Salim, and Peter Moskovits
// p. 51, Building a Simple WebSocket Server
//
// guest271314 2025
// Do What the Fuck You Want to Public License WTFPLv2 http://www.wtfpl.net/about/
class WebSocketConnection {
readable;
writable;
writer;
buffer = new ArrayBuffer(0, { maxByteLength: 1024 ** 2 });
closed = !1;
opcodes = { TEXT: 1, BINARY: 2, PING: 9, PONG: 10, CLOSE: 8 };
constructor(readable, writable, abortController) {
this.readable = readable;
this.incomingStreamController = void 0;
this.incomingStream = new ReadableStream({
start:(_) => { return this.incomingStreamController = _; }
});
this.abortController = abortController;
if (writable instanceof WritableStreamDefaultWriter) {
this.writer = writable;
} else if (writable instanceof WritableStream) {
this.writable = writable;
this.writer = this.writable.getWriter();
}
}
async processWebSocketStream() {
try {
for await (const frame of this.readable) {
// console.log(this.closed);
if (!this.closed) {
const { byteLength } = this.buffer;
// console.log(byteLength, frame.length);
this.buffer.resize(byteLength + frame.length);
const view = new DataView(this.buffer);
for (let i = 0, j = byteLength; i < frame.length; i++, j++) {
view.setUint8(j, frame.at(i));
}
await this.processFrame();
} else {
break;
}
}
console.log("WebSocket connection closed.");
} catch (e) {
console.log(e);
console.trace();
}
}
async processFrame() {
let length, maskBytes;
const buf = new Uint8Array(this.buffer), view = new DataView(buf.buffer);
if (buf.length < 2) {
return !1;
}
let idx = 2,
b1 = view.getUint8(0),
fin = b1 & 128,
opcode = b1 & 15,
b2 = view.getUint8(1),
mask = b2 & 128;
length = b2 & 127;
if (length > 125) {
if (buf.length < 8) {
return !1;
}
if (length == 126) {
length = view.getUint16(2, !1);
idx += 2;
} else if (length == 127) {
if (view.getUint32(2, !1) != 0) {
await this.close(1009, "");
}
length = view.getUint32(6, !1);
idx += 8;
}
}
if (buf.length < idx + 4 + length) {
return !1;
}
maskBytes = buf.subarray(idx, idx + 4);
idx += 4;
let payload = buf.subarray(idx, idx + length);
payload = this.unmask(maskBytes, payload);
// await this.handleFrame(opcode, payload);
this.incomingStreamController.enqueue({ opcode, payload });
if (this.buffer.byteLength === 0 && this.closed) {
try {
return !0;
} catch (e) {
console.log(e);
}
}
if (idx + length === 0) {
console.log(`this.buffer.length: ${this.buffer.byteLength}.`);
return !1;
}
// console.log(this.buffer.byteLength, idx, length);
for (let i = 0, j = idx + length; j < this.buffer.byteLength; i++, j++) {
view.setUint8(i, view.getUint8(j));
}
this.buffer.resize(this.buffer.byteLength - (idx + length));
return !0;
}
async handleFrame(opcode, buffer) {
// console.log({ opcode, length: buffer.length });
const view = new DataView(buffer.bufhttps://gist.github.com/guest271314/d330c7cea513f12ef7bf523c56431453fer);
let payload;
switch (opcode) {
case this.opcodes.TEXT:
payload = buffer;
await this.writeFrame(opcode, payload);
break;
case this.opcodes.BINARY:
payload = buffer;
await this.writeFrame(opcode, payload);
break;
case this.opcodes.PING:
await this.writeFrame(this.opcodes.PONG, buffer);
break;
case this.opcodes.PONG:
break;
case this.opcodes.CLOSE:
let code, reason;
if (buffer.length >= 2) {
code = view.getUint16(0, !1);
reason = buffer.subarray(2);
}
await this.close(code, reason);
console.log("Close opcode.", new TextDecoder().decode(reason));
this.incomingStreamController.close();
break;
default:
await this.close(1002, "unknown opcode");
}
}
async writeFrame(opcode, payload) {
await this.writer.ready;
return this.writer.write(this.encodeMessage(opcode, payload))
.catch(console.log);
}
async send(obj) {
// console.log({ obj });
let opcode, payload;
if (obj instanceof Uint8Array) {
opcode = this.opcodes.BINARY;
payload = obj;
} else if (typeof obj == "string") {
opcode = this.opcodes.TEXT;
payload = obj;
} else {
throw new Error("Cannot send object. Must be string or Uint8Array");
}
await this.writeFrame(opcode, payload);
}
async close(code, reason) {
const opcode = this.opcodes.CLOSE;
let buffer;
if (code) {
buffer = new Uint8Array(reason.length + 2);
const view = new DataView(buffer.buffer);
view.setUint16(0, code, !1);
buffer.set(reason, 2);
} else {
buffer = new Uint8Array(0);
}
console.log({ opcode, reason, buffer }, new TextDecoder().decode(reason));
await this.writeFrame(opcode, buffer);
await this.writer.close();
await this.writer.closed;
this.buffer.resize(0);
this.closed = !0;
}
unmask(maskBytes2, data) {
let payload = new Uint8Array(data.length);
for (let i = 0; i < data.length; i++) {
payload[i] = maskBytes2[i % 4] ^ data[i];
}
return payload;
}
encodeMessage(opcode, payload) {
let buf, b1 = 128 | opcode, b2 = 0, length = payload.length;
if (length < 126) {
buf = new Uint8Array(payload.length + 2 + 0);
const view = new DataView(buf.buffer);
b2 |= length;
view.setUint8(0, b1);
view.setUint8(1, b2);
buf.set(payload, 2);
} else if (length < 65536) {
buf = new Uint8Array(payload.length + 2 + 2);
const view = new DataView(buf.buffer);
b2 |= 126;
view.setUint8(0, b1);
view.setUint8(1, b2);
view.setUint16(2, length);
buf.set(payload, 4);
} else {
buf = new Uint8Array(payload.length + 2 + 8);
const view = new DataView(buf.buffer);
b2 |= 127;
view.setUint8(0, b1);
view.setUint8(1, b2);
view.setUint32(2, 0, !1);
view.setUint32(6, length, !1);
buf.set(payload, 10);
}
return buf;
}
static KEY_SUFFIX = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
static async hashWebSocketKey(secKeyWebSocket, writable) {
// Use Web Cryptography API crypto.subtle where defined
console.log(secKeyWebSocket, globalThis?.crypto?.subtle);
const encoder = new TextEncoder();
if (globalThis?.crypto?.subtle) {
const key = btoa(
[
...new Uint8Array(
await crypto.subtle.digest(
"SHA-1",
encoder.encode(
`${secKeyWebSocket}${WebSocketConnection.KEY_SUFFIX}`,
),
),
),
].map((s) => String.fromCodePoint(s)).join(""),
);
const header = `HTTP/1.1 101 Web Socket Protocol Handshake\r
Upgrade: WebSocket\r
Connection: Upgrade\r
sec-websocket-accept: ` + key + `\r
\r
`;
return writable instanceof WritableStream
? (new Response(header)).body.pipeTo(writable, { preventClose: !0 })
: writable.write(encoder.encode(header));
} else {
// txiki.js does not support Web Cryptography API crypto.subtle
// Use txiki.js specific tjs:hashing or
// https://raw.githubusercontent.com/kawanet/sha1-uint8array/main/lib/sha1-uint8array.ts
const { createHash } = await import("./sha1-uint8array.min.js");
const encoder = new TextEncoder();
const hash = createHash("sha1").update(
`${secKeyWebSocket}${WebSocketConnection.KEY_SUFFIX}`,
).digest();
const key = btoa(
String.fromCodePoint(...hash),
);
const header = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n" +
"Upgrade: WebSocket\r\n" +
"Connection: Upgrade\r\n" +
"Sec-Websocket-Accept: " + key + "\r\n\r\n";
const encoded = encoder.encode(header);
return writable instanceof WritableStream
? new Response(encode).body.pipeTo(writable, { preventClose: !0 })
: writable.write(encoded);
}
}
}
export { WebSocketConnection };
Usage.
Here in Chromium browser in an Isolated Web App using Direct Sockets TCPServerSocket, in pertinent part, the server implemented in Chromium browser
client.pipeTo(
new WritableStream({
start: (controller) => {
this.ws = void 0;
const {
readable: wsReadable,
writable: wsWritable
} = new TransformStream,
wsWriter = wsWritable.getWriter();
Object.assign(this, {
wsReadable,
wsWritable,
wsWriter
});
},
write: async (r, controller) => {
const request = decoder.decode(r);
// console.log(request);
// Handle WebSocket
if (!/(GET|POST|HEAD|OPTIONS|QUERY)/i.test(request)) {
await this.wsWriter.ready;
await this.wsWriter.write(r);
}
// Handle WebSocket request
if (/^GET/.test(request) && /websocket/i.test(request)) {
const {
headers,
method,
uri,
servertype
} = getHeaders(
request,
);
const key = headers.get("sec-websocket-key");
await WebSocketConnection.hashWebSocketKey(
key,
writer,
);
this.ws = new WebSocketConnection(this.wsReadable, writer);
this.ws.processWebSocketStream().catch((e) => {
throw e;
});
console.log(this.ws);
this.ws.incomingStream.pipeTo(
new WritableStream({
write: async ({
opcode,
payload
}) => {
await this.ws.handleFrame(opcode, payload);
if (opcode === 8) {
this.ws.incomingStreamController.close();
}
}
})
).catch(() => {
console.log(`Incoming stream closed`)
});
}
The client using WebSocketStream, in Chromium browser
// Only aborts *before* the handshake
var abortable = new AbortController();
var { signal } = abortable;
var wss = new WebSocketStream("ws://127.0.0.1:44818", {
signal,
});
console.log(wss);
var { readable, writable } = await wss.opened.catch(console.warn);
var connection = wss.closed.then(({ closeCode, reason }) => {
return `WebSocketStream closed. closeCode: ${closeCode}, reason: ${reason}`;
}).catch((e) => {
return e.message;
});
var writer = writable.getWriter();
var reader = readable.getReader();
var len = 0;
var encoder = new TextEncoder();
var decoder = new TextDecoder();
var data = new Uint8Array(1024 ** 2).fill(97);
var len = 0;
for (let i = 0; i < data.length; i += 65536) {
try {
await writer.ready;
await writer.write(data.subarray(i, i + 65536));
// console.log(writer.desiredSize);
const { value: v, done } = await reader.read();
if (typeof v === "string") {
console.log(v);
} else {
const decoded = decoder.decode(v, {
stream: true,
});
console.log(len += v.byteLength, v, [...decoded].every((s) => s === "a"));
}
} catch (e) {
console.warn(e);
}
}
console.assert(len === data.buffer.byteLength, [len, data.buffer.byteLength]);
console.log(len, data.buffer.byteLength);
await writer.ready;
await writer.write("Text").then(() => reader.read()).then(console.log).catch(
console.warn,
);
try {
writer.releaseLock();
reader.releaseLock();
wss.close({
closeCode: 1000,
reason: "Done streaming",
});
// await writer.close();
// await writer.closed;
} catch {}
function handleClose(args) {
return args;
}
await Promise.allSettled([
reader.closed.then(
handleClose.bind(null, `readable.locked ${readable.locked}`),
).catch(handleClose.bind(null, `readable.locked ${readable.locked}`)),
writer.closed.then(
handleClose.bind(null, `writable.locked ${writable.locked}`),
).catch(handleClose.bind(null, `writable.locked ${writable.locked}`)),
connection,
]).then((result) => console.log(result));
```