Thanks to visit codestin.com
Credit goes to github.com

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

Conversation

Parkreiner
Copy link
Member

@Parkreiner Parkreiner commented Mar 7, 2025

Closes #16777
Part 2/2, with #16853 handling the backend changes

Changes made

  • Added OneWayWebSocket utility class to help enforce one-way communication from the server to the client
  • Updated all client client code to use the new WebSocket-based endpoints made to replace the current SSE-based endpoints
  • Updated WebSocket event handlers to be aware of new protocols
  • Refactored existing useEffect calls and removed some synchronization bugs
  • Removed dependencies and types for dealing with SSEs
  • Addressed some minor Biome warnings

Notes

  • It is a little bit weird to be sending payloads name "ServerSentEvent" over WebSockets, but all the backend engineers I talked to said that it was fine for an initial step towards removing SSEs. I actually think there is value in sending structured data to the client, so the right move might actually be to just rename the type

@Parkreiner Parkreiner self-assigned this Mar 7, 2025
@Parkreiner Parkreiner changed the base branch from main to mes/one-way-ws-01 March 7, 2025 23:11

const connect = (): (() => void) => {
const source = watchAgentMetadata(agent.id);
const createNewConnection = () => {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The previous create function was super weird because it was responsible for three things

  • Creating a new WebSocket connection from the outside effect
  • Being able to create new connections by calling itself recursively
  • Also acting as a useEffect cleanup function

Figured it'd be better to split up those responsibilities

Comment on lines +12 to +16
const [cachedVersionId, setCachedVersionId] = useState(templateVersionId);
if (cachedVersionId !== templateVersionId) {
setCachedVersionId(templateVersionId);
setLogs([]);
}
Copy link
Member Author

@Parkreiner Parkreiner Mar 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is needed to remove Biome's useExhaustiveDependencies warning without introducing unnecessary renders

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is in regards to useEffect? I think if applicable it'd be better to use a biome comment to ignore linting on the line with an explanation why

	 // biome-ignore lint/correctness/useExhaustiveDependencies: reason
	 useEffect(() => {
	 if (cachedVersionId !== templateVersionId) {
	 	setCachedVersionId(templateVersionId);
	 	setLogs([]);
	 }
	 	
	 }, [...]);

Copy link
Member Author

@Parkreiner Parkreiner Mar 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, the linter warning is actually valid – the useEffect approach introduces additional renders and has a risk of screen flickering, while the if statement approach avoids all of that

Was mainly calling it out, because while it's the "correct" way to do things (and is shouted out in the React docs), I do think it's an ugly pattern, and you need a lot of context for how React works under the hood to know why it's better than the useEffect approach

};
}, [options?.onDone, templateVersionId, templateVersionStatus]);
return () => socket.close();
}, [stableOnDone, canWatch, templateVersionId]);
Copy link
Member Author

@Parkreiner Parkreiner Mar 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The previous version of the effect was re-synchronizing way too often, because we put a function without a stable memory reference and templateVersionStatus directly into the dependency array:

  • Function: Every render would create a new reference and destroy the existing connection for no good reason
  • Status: The value can be a lot of things that indicate that we shouldn't watch (including undefined). Plus, if the status ever switches from pending to running, that's a new value for the array, and triggers a re-sync, destroying a perfectly good connection

Best option seemed to be to wrap the callback, and also collapse the status to a boolean

@Parkreiner Parkreiner requested a review from brettkolodny March 7, 2025 23:34
@@ -1080,7 +1081,7 @@ class ApiMethods {
};

getWorkspaceByOwnerAndName = async (
username = "me",
username: string,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Biome was complaining about having default parameters not be at the end of the function signature, but also, we were never calling these functions with an explicit value of undefined

@Parkreiner
Copy link
Member Author

@brettkolodny Pinging you to make sure this PR didn't miss you, but I caught some Storybook issues that I'm going to try to fix up as soon as I'm done with some meetings

I'll post again once the PR is ready

if (storybookMetadata !== undefined) {
setMetadata(storybookMetadata);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was removed because it introduces unnecessary renders – better to just use storybookMetadata as the initial value for the state and avoid a bunch of state syncs

onError: (error) => console.error(error),
onDone: stableOnDone,
onMessage: (newLog) => {
setLogs((current) => [...(current ?? []), newLog]);
Copy link
Member Author

@Parkreiner Parkreiner Mar 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really wanted to just make sure that the logs is always an array, and get rid of the undefined case to make the state updates cleaner, but a bunch of our other components currently depends on that setup

Copy link
Contributor

@brettkolodny brettkolodny left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall looks good. I'm not used to using type assertions as much as coder does. I think that the "contract" so to speak between the frontend and backend via the generated types and their use within the queries/api code is fine though, and I wonder if we can get the same "contract" for this websocket class. Or maybe that's not worth the effort

Comment on lines +12 to +16
const [cachedVersionId, setCachedVersionId] = useState(templateVersionId);
if (cachedVersionId !== templateVersionId) {
setCachedVersionId(templateVersionId);
setLogs([]);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is in regards to useEffect? I think if applicable it'd be better to use a biome comment to ignore linting on the line with an explanation why

	 // biome-ignore lint/correctness/useExhaustiveDependencies: reason
	 useEffect(() => {
	 if (cachedVersionId !== templateVersionId) {
	 	setCachedVersionId(templateVersionId);
	 	setLogs([]);
	 }
	 	
	 }, [...]);

);
export const watchAgentMetadata = (
agentId: string,
): OneWayWebSocket<TypesGen.ServerSentEvent> => {
Copy link
Member Author

@Parkreiner Parkreiner Mar 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sadly, this is the best we can do as far as giving ourselves type-safety (without updating GUTS). It'll be less of an issue for WebSocket connections that don't send SSE-formatted events, but the ServerSentEvent type is structured like this:

// From codersdk/serversentevents.go
export interface ServerSentEvent {
	readonly type: ServerSentEventType;
	// empty interface{} type, falling back to unknown
	readonly data: unknown;
}

There's no way to pass a type parameter to the type and make data more specific

@Parkreiner Parkreiner requested a review from brettkolodny March 12, 2025 20:40
@Parkreiner
Copy link
Member Author

@brettkolodny Running into some E2E issues, but I'm still going to request a re-review now because:

  1. The error seems to be for code that has nothing to do with what I touched
  2. The error is a timeout for the GitHub auth logic. I've noticed that GitHub's been really slow for me today, so I'm wondering if this could be connected. I'm going to try again later tonight

@brettkolodny
Copy link
Contributor

Approved for when those tests pass. I've also been having some issues with GitHub today so it could definitely be that

@Parkreiner
Copy link
Member Author

Okay, it was a GitHub hiccup – things are working fine now

@Parkreiner
Copy link
Member Author

Bumping this so it doesn't get closed. Trying to get a backend engineer on the other PR as soon as I can

@Parkreiner Parkreiner merged commit 8f1eca0 into mes/one-way-ws-01 Mar 28, 2025
26 of 30 checks passed
@Parkreiner Parkreiner deleted the mes/one-way-ws-02 branch March 28, 2025 20:17
@github-actions github-actions bot locked and limited conversation to collaborators Mar 28, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants