convex
Version:
Client for the Convex Cloud
107 lines (93 loc) • 3.13 kB
text/typescript
import { Base64 } from "../../values/index.js";
import { Long } from "../long.js";
// --experimental-vm-modules which we use for jest doesn't support named exports
import WebSocket, { WebSocketServer } from "ws";
// Let's pretend this ws WebSocket is a browser WebSocket (it's very close)
export const nodeWebSocket = WebSocket as unknown as typeof window.WebSocket;
import { ClientMessage, ServerMessage } from "./protocol.js";
export type InMemoryWebSocketTest = (args: {
address: string;
socket: () => WebSocket;
receive: () => Promise<ClientMessage>;
send: (message: ServerMessage) => void;
close: () => void;
}) => Promise<void>;
function listeningSocketServer(): Promise<WebSocketServer> {
return new Promise((resolve) => {
const wss = new WebSocketServer({ port: 0 });
wss.on("listening", () => resolve(wss));
});
}
// Run a test with a real node WebSocket instance connected
export async function withInMemoryWebSocket(
cb: InMemoryWebSocketTest,
debug = false,
) {
// These state variables are consistent over multiple sockets.
let received: (msg: string) => void;
// prettier-ignore
const messages: Promise<string>[] = [ new Promise((r) => { received = r; }) ];
let socket: WebSocket | null = null;
const wss = await listeningSocketServer();
const setUpSocket = () => {
wss.once("connection", function connection(ws: WebSocket) {
socket = ws;
ws.on("message", function message(data: string) {
received(data);
// eslint-disable-next-line no-console
if (debug) console.debug(`client --${JSON.parse(data).type}--> `);
messages.push(
new Promise((r) => {
received = r;
}),
);
});
});
};
setUpSocket();
// receive and send are stable across multiple socket connections
async function receive(): Promise<ClientMessage> {
const msgP = messages.shift();
if (!msgP) {
throw new Error("Receive() called twice? No message promise found.");
}
return JSON.parse(await msgP);
}
function send(message: ServerMessage) {
// eslint-disable-next-line no-console
if (debug) console.debug(` <--${message.type}-- server`);
socket!.send(encodeServerMessage(message));
}
const s: any = wss.address();
const address = typeof s === "string" ? s : `http://127.0.0.1:${s.port}`;
try {
await cb({
address,
socket: () => socket as unknown as WebSocket,
receive,
send,
close: () => {
// eslint-disable-next-line no-console
if (debug) console.debug(` -->8-CLOSE- server`);
socket!.close();
setUpSocket();
},
});
} finally {
socket!.close();
wss.close();
}
}
export function encodeServerMessage(message: ServerMessage): string {
function replacer(_key: string, value: any) {
if (Long.isLong(value)) {
return encodeLong(value);
}
return value;
}
return JSON.stringify(message, replacer);
}
function encodeLong(n: Long) {
const integerBytes = Uint8Array.from(n.toBytesLE());
return Base64.fromByteArray(integerBytes);
}