-
Notifications
You must be signed in to change notification settings - Fork 881
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
Conversation
|
||
const connect = (): (() => void) => { | ||
const source = watchAgentMetadata(agent.id); | ||
const createNewConnection = () => { |
There was a problem hiding this comment.
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
const [cachedVersionId, setCachedVersionId] = useState(templateVersionId); | ||
if (cachedVersionId !== templateVersionId) { | ||
setCachedVersionId(templateVersionId); | ||
setLogs([]); | ||
} |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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([]);
}
}, [...]);
There was a problem hiding this comment.
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]); |
There was a problem hiding this comment.
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
@@ -1080,7 +1081,7 @@ class ApiMethods { | |||
}; | |||
|
|||
getWorkspaceByOwnerAndName = async ( | |||
username = "me", | |||
username: string, |
There was a problem hiding this comment.
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
@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); |
There was a problem hiding this comment.
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]); |
There was a problem hiding this comment.
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
There was a problem hiding this 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
const [cachedVersionId, setCachedVersionId] = useState(templateVersionId); | ||
if (cachedVersionId !== templateVersionId) { | ||
setCachedVersionId(templateVersionId); | ||
setLogs([]); | ||
} |
There was a problem hiding this comment.
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> => { |
There was a problem hiding this comment.
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
@brettkolodny Running into some E2E issues, but I'm still going to request a re-review now because:
|
Approved for when those tests pass. I've also been having some issues with GitHub today so it could definitely be that |
Okay, it was a GitHub hiccup – things are working fine now |
Bumping this so it doesn't get closed. Trying to get a backend engineer on the other PR as soon as I can |
Closes #16777
Part 2/2, with #16853 handling the backend changes
Changes made
OneWayWebSocket
utility class to help enforce one-way communication from the server to the clientuseEffect
calls and removed some synchronization bugsNotes