Skip to content
Prev Previous commit
Next Next commit
refactor: centralize socket implementation
  • Loading branch information
Parkreiner committed Aug 5, 2025
commit c2d07fbe2ffcf4d9352440861dfd95831f4b10c7
153 changes: 8 additions & 145 deletions site/src/utils/OneWayWebSocket.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,145 +7,8 @@
* WebSocket class doesn't have any bugs, and can safely be mocked out.
*/

import {
type OneWayMessageEvent,
OneWayWebSocket,
type WebSocketEventType,
} from "./OneWayWebSocket";

type MockPublisher = Readonly<{
publishMessage: (event: MessageEvent<string>) => void;
publishError: (event: ErrorEvent) => void;
publishClose: (event: CloseEvent) => void;
publishOpen: (event: Event) => void;
}>;

function createMockWebSocket(
url: string,
protocols?: string | string[],
): readonly [WebSocket, MockPublisher] {
type EventMap = {
message: MessageEvent<string>;
error: ErrorEvent;
close: CloseEvent;
open: Event;
};
type CallbackStore = {
[K in keyof EventMap]: ((event: EventMap[K]) => void)[];
};

let activeProtocol: string;
if (Array.isArray(protocols)) {
activeProtocol = protocols[0] ?? "";
} else if (typeof protocols === "string") {
activeProtocol = protocols;
} else {
activeProtocol = "";
}

let closed = false;
const store: CallbackStore = {
message: [],
error: [],
close: [],
open: [],
};

const mockSocket: WebSocket = {
CONNECTING: 0,
OPEN: 1,
CLOSING: 2,
CLOSED: 3,

url,
protocol: activeProtocol,
readyState: 1,
binaryType: "blob",
bufferedAmount: 0,
extensions: "",
onclose: null,
onerror: null,
onmessage: null,
onopen: null,
send: jest.fn(),
dispatchEvent: jest.fn(),

addEventListener: <E extends WebSocketEventType>(
eventType: E,
callback: WebSocketEventMap[E],
) => {
if (closed) {
return;
}

const subscribers = store[eventType];
const cb = callback as unknown as CallbackStore[E][0];
if (!subscribers.includes(cb)) {
subscribers.push(cb);
}
},

removeEventListener: <E extends WebSocketEventType>(
eventType: E,
callback: WebSocketEventMap[E],
) => {
if (closed) {
return;
}

const subscribers = store[eventType];
const cb = callback as unknown as CallbackStore[E][0];
if (subscribers.includes(cb)) {
const updated = store[eventType].filter((c) => c !== cb);
store[eventType] = updated as unknown as CallbackStore[E];
}
},

close: () => {
closed = true;
},
};

const publisher: MockPublisher = {
publishOpen: (event) => {
if (closed) {
return;
}
for (const sub of store.open) {
sub(event);
}
},

publishError: (event) => {
if (closed) {
return;
}
for (const sub of store.error) {
sub(event);
}
},

publishMessage: (event) => {
if (closed) {
return;
}
for (const sub of store.message) {
sub(event);
}
},

publishClose: (event) => {
if (closed) {
return;
}
for (const sub of store.close) {
sub(event);
}
},
};

return [mockSocket, publisher] as const;
}
import { createMockWebSocket, MockWebSocketServer } from "testHelpers/websockets";
import { type OneWayMessageEvent, OneWayWebSocket } from "./OneWayWebSocket";

describe(OneWayWebSocket.name, () => {
const dummyRoute = "/api/v2/blah";
Expand All @@ -167,7 +30,7 @@ describe(OneWayWebSocket.name, () => {
});

it("Lets a consumer add an event listener of each type", () => {
let publisher!: MockPublisher;
let publisher!: MockWebSocketServer;
const oneWay = new OneWayWebSocket({
apiRoute: dummyRoute,
websocketInit: (url, protocols) => {
Expand Down Expand Up @@ -207,7 +70,7 @@ describe(OneWayWebSocket.name, () => {
});

it("Lets a consumer remove an event listener of each type", () => {
let publisher!: MockPublisher;
let publisher!: MockWebSocketServer;
const oneWay = new OneWayWebSocket({
apiRoute: dummyRoute,
websocketInit: (url, protocols) => {
Expand Down Expand Up @@ -252,7 +115,7 @@ describe(OneWayWebSocket.name, () => {
});

it("Only calls each callback once if callback is added multiple times", () => {
let publisher!: MockPublisher;
let publisher!: MockWebSocketServer;
const oneWay = new OneWayWebSocket({
apiRoute: dummyRoute,
websocketInit: (url, protocols) => {
Expand Down Expand Up @@ -294,7 +157,7 @@ describe(OneWayWebSocket.name, () => {
});

it("Lets consumers register multiple callbacks for each event type", () => {
let publisher!: MockPublisher;
let publisher!: MockWebSocketServer;
const oneWay = new OneWayWebSocket({
apiRoute: dummyRoute,
websocketInit: (url, protocols) => {
Expand Down Expand Up @@ -375,7 +238,7 @@ describe(OneWayWebSocket.name, () => {
});

it("Gives consumers pre-parsed versions of message events", () => {
let publisher!: MockPublisher;
let publisher!: MockWebSocketServer;
const oneWay = new OneWayWebSocket({
apiRoute: dummyRoute,
websocketInit: (url, protocols) => {
Expand Down Expand Up @@ -405,7 +268,7 @@ describe(OneWayWebSocket.name, () => {
});

it("Exposes parsing error if message payload could not be parsed as JSON", () => {
let publisher!: MockPublisher;
let publisher!: MockWebSocketServer;
const oneWay = new OneWayWebSocket({
apiRoute: dummyRoute,
websocketInit: (url, protocols) => {
Expand Down