Skip to content

chore: add support for one-way WebSockets to UI #16855

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 24 commits into from
Mar 28, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
b8cfe76
feat: add support for one-way websockets to UI
Parkreiner Mar 7, 2025
367906d
fix: apply formatting
Parkreiner Mar 7, 2025
09f5e95
docs: remove outdated comment
Parkreiner Mar 7, 2025
04a3846
fix: add missing clear call
Parkreiner Mar 7, 2025
ca8e94f
fix: streamline biome fixes
Parkreiner Mar 7, 2025
81a723a
fix: resolve Storybook metadata setup bug
Parkreiner Mar 12, 2025
4364a3d
docs: make warning more obvious
Parkreiner Mar 12, 2025
ecb2940
fix: beef up socket retry logic
Parkreiner Mar 12, 2025
bfe4d9f
fix: make it harder to initialize OWWS
Parkreiner Mar 12, 2025
6cdfc21
fix: apply feedback
Parkreiner Mar 12, 2025
682e2f4
fix: update JSDoc
Parkreiner Mar 12, 2025
20ad778
Merge branch 'main' into mes/one-way-ws-02
Parkreiner Mar 18, 2025
9b19ceb
chore: add missing socket unit tests
Parkreiner Mar 18, 2025
423910f
fix: update notifications code to use OWWS
Parkreiner Mar 18, 2025
247dbb6
Merge branch 'mes/one-way-ws-01' into mes/one-way-ws-02
Parkreiner Mar 18, 2025
6e3e0d8
fix: remove comment about what to test
Parkreiner Mar 18, 2025
c422379
fix: make class fields readonly
Parkreiner Mar 19, 2025
0824dd4
fix: sort imports
Parkreiner Mar 19, 2025
db448d7
Merge branch 'mes/one-way-ws-01' into mes/one-way-ws-02
Parkreiner Mar 19, 2025
60bf505
Merge branch 'mes/one-way-ws-01' into mes/one-way-ws-02
Parkreiner Mar 25, 2025
c1cee57
refactor: make tests more maintainable
Parkreiner Mar 25, 2025
70b74e2
fix: remove unused type alias
Parkreiner Mar 25, 2025
8e34e91
fix: make mock publisher more robust
Parkreiner Mar 25, 2025
8db068a
Merge branch 'mes/one-way-ws-01' into mes/one-way-ws-02
Parkreiner Mar 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
refactor: make tests more maintainable
  • Loading branch information
Parkreiner committed Mar 25, 2025
commit c1cee57f0e8803768ea19f4f5493ecf1da6e5578
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,10 @@ export const NotificationsInbox: FC<NotificationsInboxProps> = ({
}

const msg = e.parsedMessage;
updateNotificationsCache((prev) => {
updateNotificationsCache((current) => {
return {
unread_count: msg.unread_count,
notifications: [msg.notification, ...prev.notifications],
notifications: [msg.notification, ...current.notifications],
};
});
});
Expand Down
116 changes: 71 additions & 45 deletions site/src/utils/OneWayWebSocket.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,21 @@ import {
type WebSocketEventType,
} from "./OneWayWebSocket";

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

type MockSocket = WebSocket & {
publisher: MockPublisher;
};

function createMockWebSocket(
url: string,
protocols?: string | string[],
): MockSocket {
): readonly [WebSocket, MockPublisher] {
type EventMap = {
message: MessageEvent<string>;
error: ErrorEvent;
Expand Down Expand Up @@ -51,7 +55,7 @@ function createMockWebSocket(
open: [],
};

return {
const mockSocket: WebSocket = {
CONNECTING: 0,
OPEN: 1,
CLOSING: 2,
Expand Down Expand Up @@ -104,7 +108,9 @@ function createMockWebSocket(
close: () => {
closed = true;
},
};

const publisher: MockPublisher = {
publishOpen: (event) => {
for (const sub of store.open) {
sub(event);
Expand All @@ -129,6 +135,8 @@ function createMockWebSocket(
}
},
};

return [mockSocket, publisher] as const;
}

describe(OneWayWebSocket.name, () => {
Expand All @@ -141,19 +149,22 @@ describe(OneWayWebSocket.name, () => {
expect(() => {
new OneWayWebSocket({
apiRoute: r,
websocketInit: createMockWebSocket,
websocketInit: (url, protocols) => {
const [socket] = createMockWebSocket(url, protocols);
return socket;
},
});
}).toThrow(Error);
}
});

it("Lets a consumer add an event listener of each type", () => {
let mock!: MockSocket;
let publisher!: MockPublisher;
const oneWay = new OneWayWebSocket({
apiRoute: dummyRoute,
websocketInit: (url, protocols) => {
const socket = createMockWebSocket(url, protocols);
mock = socket;
const [socket, pub] = createMockWebSocket(url, protocols);
publisher = pub;
return socket;
},
});
Expand All @@ -168,14 +179,14 @@ describe(OneWayWebSocket.name, () => {
oneWay.addEventListener("error", onError);
oneWay.addEventListener("message", onMessage);

mock.publishOpen(new Event("open"));
mock.publishClose(new CloseEvent("close"));
mock.publishError(
publisher.publishOpen(new Event("open"));
publisher.publishClose(new CloseEvent("close"));
publisher.publishError(
new ErrorEvent("error", {
error: new Error("Whoops - connection broke"),
}),
);
mock.publishMessage(
publisher.publishMessage(
new MessageEvent("message", {
data: "null",
}),
Expand All @@ -188,12 +199,12 @@ describe(OneWayWebSocket.name, () => {
});

it("Lets a consumer remove an event listener of each type", () => {
let mock!: MockSocket;
let publisher!: MockPublisher;
const oneWay = new OneWayWebSocket({
apiRoute: dummyRoute,
websocketInit: (url, protocols) => {
const socket = createMockWebSocket(url, protocols);
mock = socket;
const [socket, pub] = createMockWebSocket(url, protocols);
publisher = pub;
return socket;
},
});
Expand All @@ -213,14 +224,14 @@ describe(OneWayWebSocket.name, () => {
oneWay.removeEventListener("error", onError);
oneWay.removeEventListener("message", onMessage);

mock.publishOpen(new Event("open"));
mock.publishClose(new CloseEvent("close"));
mock.publishError(
publisher.publishOpen(new Event("open"));
publisher.publishClose(new CloseEvent("close"));
publisher.publishError(
new ErrorEvent("error", {
error: new Error("Whoops - connection broke"),
}),
);
mock.publishMessage(
publisher.publishMessage(
new MessageEvent("message", {
data: "null",
}),
Expand All @@ -233,12 +244,12 @@ describe(OneWayWebSocket.name, () => {
});

it("Only calls each callback once if callback is added multiple times", () => {
let mock!: MockSocket;
let publisher!: MockPublisher;
const oneWay = new OneWayWebSocket({
apiRoute: dummyRoute,
websocketInit: (url, protocols) => {
const socket = createMockWebSocket(url, protocols);
mock = socket;
const [socket, pub] = createMockWebSocket(url, protocols);
publisher = pub;
return socket;
},
});
Expand All @@ -255,14 +266,14 @@ describe(OneWayWebSocket.name, () => {
oneWay.addEventListener("message", onMessage);
}

mock.publishOpen(new Event("open"));
mock.publishClose(new CloseEvent("close"));
mock.publishError(
publisher.publishOpen(new Event("open"));
publisher.publishClose(new CloseEvent("close"));
publisher.publishError(
new ErrorEvent("error", {
error: new Error("Whoops - connection broke"),
}),
);
mock.publishMessage(
publisher.publishMessage(
new MessageEvent("message", {
data: "null",
}),
Expand All @@ -275,12 +286,12 @@ describe(OneWayWebSocket.name, () => {
});

it("Lets consumers register multiple callbacks for each event type", () => {
let mock!: MockSocket;
let publisher!: MockPublisher;
const oneWay = new OneWayWebSocket({
apiRoute: dummyRoute,
websocketInit: (url, protocols) => {
const socket = createMockWebSocket(url, protocols);
mock = socket;
const [socket, pub] = createMockWebSocket(url, protocols);
publisher = pub;
return socket;
},
});
Expand All @@ -303,14 +314,14 @@ describe(OneWayWebSocket.name, () => {
oneWay.addEventListener("error", onError2);
oneWay.addEventListener("message", onMessage2);

mock.publishOpen(new Event("open"));
mock.publishClose(new CloseEvent("close"));
mock.publishError(
publisher.publishOpen(new Event("open"));
publisher.publishClose(new CloseEvent("close"));
publisher.publishError(
new ErrorEvent("error", {
error: new Error("Whoops - connection broke"),
}),
);
mock.publishMessage(
publisher.publishMessage(
new MessageEvent("message", {
data: "null",
}),
Expand All @@ -330,15 +341,21 @@ describe(OneWayWebSocket.name, () => {
it("Computes the socket protocol based on the browser location protocol", () => {
const oneWay1 = new OneWayWebSocket({
apiRoute: dummyRoute,
websocketInit: createMockWebSocket,
websocketInit: (url, protocols) => {
const [socket] = createMockWebSocket(url, protocols);
return socket;
},
location: {
protocol: "https:",
host: "www.cool.com",
},
});
const oneWay2 = new OneWayWebSocket({
apiRoute: dummyRoute,
websocketInit: createMockWebSocket,
websocketInit: (url, protocols) => {
const [socket] = createMockWebSocket(url, protocols);
return socket;
},
location: {
protocol: "http:",
host: "www.cool.com",
Expand All @@ -350,12 +367,12 @@ describe(OneWayWebSocket.name, () => {
});

it("Gives consumers pre-parsed versions of message events", () => {
let mock!: MockSocket;
let publisher!: MockPublisher;
const oneWay = new OneWayWebSocket({
apiRoute: dummyRoute,
websocketInit: (url, protocols) => {
const socket = createMockWebSocket(url, protocols);
mock = socket;
const [socket, pub] = createMockWebSocket(url, protocols);
publisher = pub;
return socket;
},
});
Expand All @@ -371,7 +388,7 @@ describe(OneWayWebSocket.name, () => {
data: JSON.stringify(payload),
});

mock.publishMessage(event);
publisher.publishMessage(event);
expect(onMessage).toHaveBeenCalledWith({
sourceEvent: event,
parsedMessage: payload,
Expand All @@ -380,12 +397,12 @@ describe(OneWayWebSocket.name, () => {
});

it("Exposes parsing error if message payload could not be parsed as JSON", () => {
let mock!: MockSocket;
let publisher!: MockPublisher;
const oneWay = new OneWayWebSocket({
apiRoute: dummyRoute,
websocketInit: (url, protocols) => {
const socket = createMockWebSocket(url, protocols);
mock = socket;
const [socket, pub] = createMockWebSocket(url, protocols);
publisher = pub;
return socket;
},
});
Expand All @@ -397,7 +414,7 @@ describe(OneWayWebSocket.name, () => {
const event = new MessageEvent("message", {
data: payload,
});
mock.publishMessage(event);
publisher.publishMessage(event);

const arg: OneWayMessageEvent<never> = onMessage.mock.lastCall[0];
expect(arg.sourceEvent).toEqual(event);
Expand All @@ -413,7 +430,10 @@ describe(OneWayWebSocket.name, () => {
};
const oneWay1 = new OneWayWebSocket({
apiRoute: dummyRoute,
websocketInit: createMockWebSocket,
websocketInit: (url, protocols) => {
const [socket] = createMockWebSocket(url, protocols);
return socket;
},
searchParams: input1,
location: {
protocol: "https:",
Expand All @@ -429,7 +449,10 @@ describe(OneWayWebSocket.name, () => {
const input2 = new URLSearchParams(input1);
const oneWay2 = new OneWayWebSocket({
apiRoute: dummyRoute,
websocketInit: createMockWebSocket,
websocketInit: (url, protocols) => {
const [socket] = createMockWebSocket(url, protocols);
return socket;
},
searchParams: input2,
location: {
protocol: "https:",
Expand All @@ -444,7 +467,10 @@ describe(OneWayWebSocket.name, () => {

const oneWay3 = new OneWayWebSocket({
apiRoute: dummyRoute,
websocketInit: createMockWebSocket,
websocketInit: (url, protocols) => {
const [socket] = createMockWebSocket(url, protocols);
return socket;
},
searchParams: undefined,
location: {
protocol: "https:",
Expand Down
Loading