- Notifications
You must be signed in to change notification settings - Fork1.1k
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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Uh oh!
There was an error while loading.Please reload this page.
Changes fromall commits
b8cfe76367906d09f5e9504a3846ca8e94f81a723a4364a3decb2940bfe4d9f6cdfc21682e2f420ad7789b19ceb423910f247dbb66e3e0d8c4223790824dd4db448d760bf505c1cee5770b74e28e34e918db068aFile filter
Filter by extension
Conversations
Uh oh!
There was an error while loading.Please reload this page.
Jump to
Uh oh!
There was an error while loading.Please reload this page.
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more abouthow customized files appear on GitHub.
Uh oh!
There was an error while loading.Please reload this page.
This file was deleted.
Uh oh!
There was an error while loading.Please reload this page.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -22,9 +22,10 @@ | ||
| import globalAxios, { type AxiosInstance, isAxiosError } from "axios"; | ||
| import type dayjs from "dayjs"; | ||
| import userAgentParser from "ua-parser-js"; | ||
| import { OneWayWebSocket } from "utils/OneWayWebSocket"; | ||
| import { delay } from "../utils/delay"; | ||
| import type { PostWorkspaceUsageRequest } from "./typesGenerated"; | ||
| import * as TypesGen from "./typesGenerated"; | ||
| const getMissingParameters = ( | ||
| oldBuildParameters: TypesGen.WorkspaceBuildParameter[], | ||
| @@ -101,61 +102,40 @@ const getMissingParameters = ( | ||
| }; | ||
| /** | ||
| * @param agentId | ||
| * @returns {OneWayWebSocket} A OneWayWebSocket that emits Server-Sent Events. | ||
| */ | ||
| export const watchAgentMetadata = ( | ||
| agentId: string, | ||
| ): OneWayWebSocket<TypesGen.ServerSentEvent> => { | ||
ContributorAuthor
| ||
| return new OneWayWebSocket({ | ||
| apiRoute: `/api/v2/workspaceagents/${agentId}/watch-metadata-ws`, | ||
| }); | ||
| }; | ||
| /** | ||
| * @returns {OneWayWebSocket} A OneWayWebSocket that emits Server-Sent Events. | ||
| */ | ||
| export const watchWorkspace = ( | ||
| workspaceId: string, | ||
| ): OneWayWebSocket<TypesGen.ServerSentEvent> => { | ||
| return new OneWayWebSocket({ | ||
| apiRoute: `/api/v2/workspaces/${workspaceId}/watch-ws`, | ||
| }); | ||
| }; | ||
| type WatchInboxNotificationsParams =Readonly<{ | ||
| read_status?: "read" | "unread" | "all"; | ||
| }>; | ||
| export function watchInboxNotifications( | ||
| params?: WatchInboxNotificationsParams, | ||
| ): OneWayWebSocket<TypesGen.GetInboxNotificationResponse> { | ||
| return new OneWayWebSocket({ | ||
| apiRoute: "/api/v2/notifications/inbox/watch", | ||
| searchParams: params, | ||
| }); | ||
| } | ||
| export const getURLWithSearchParams = ( | ||
| basePath: string, | ||
| @@ -1125,7 +1105,7 @@ class ApiMethods { | ||
| }; | ||
| getWorkspaceByOwnerAndName = async ( | ||
| username: string, | ||
ContributorAuthor There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
| workspaceName: string, | ||
| params?: TypesGen.WorkspaceOptions, | ||
| ): Promise<TypesGen.Workspace> => { | ||
| @@ -1138,7 +1118,7 @@ class ApiMethods { | ||
| }; | ||
| getWorkspaceBuildByNumber = async ( | ||
| username: string, | ||
| workspaceName: string, | ||
| buildNumber: number, | ||
| ): Promise<TypesGen.WorkspaceBuild> => { | ||
| @@ -1324,7 +1304,7 @@ class ApiMethods { | ||
| }; | ||
| createWorkspace = async ( | ||
| userId: string, | ||
| workspace: TypesGen.CreateWorkspaceRequest, | ||
| ): Promise<TypesGen.Workspace> => { | ||
| const response = await this.axios.post<TypesGen.Workspace>( | ||
| @@ -2542,7 +2522,7 @@ function createWebSocket( | ||
| ) { | ||
| const protocol = location.protocol === "https:" ? "wss:" : "ws:"; | ||
| const socket = new WebSocket( | ||
| `${protocol}//${location.host}${path}?${params}`, | ||
| ); | ||
| socket.binaryType = "blob"; | ||
| return socket; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -3,9 +3,11 @@ import Skeleton from "@mui/material/Skeleton"; | ||
| import Tooltip from "@mui/material/Tooltip"; | ||
| import { watchAgentMetadata } from "api/api"; | ||
| import type { | ||
| ServerSentEvent, | ||
| WorkspaceAgent, | ||
| WorkspaceAgentMetadata, | ||
| } from "api/typesGenerated"; | ||
| import { displayError } from "components/GlobalSnackbar/utils"; | ||
| import { Stack } from "components/Stack/Stack"; | ||
| import dayjs from "dayjs"; | ||
| import { | ||
| @@ -17,6 +19,7 @@ import { | ||
| useState, | ||
| } from "react"; | ||
| import { MONOSPACE_FONT_FAMILY } from "theme/constants"; | ||
| import type { OneWayWebSocket } from "utils/OneWayWebSocket"; | ||
| type ItemStatus = "stale" | "valid" | "loading"; | ||
| @@ -42,58 +45,90 @@ interface AgentMetadataProps { | ||
| storybookMetadata?: WorkspaceAgentMetadata[]; | ||
| } | ||
| const maxSocketErrorRetryCount = 3; | ||
| export const AgentMetadata: FC<AgentMetadataProps> = ({ | ||
| agent, | ||
| storybookMetadata, | ||
| }) => { | ||
| const [activeMetadata, setActiveMetadata] = useState(storybookMetadata); | ||
| useEffect(() => { | ||
| // This is an unfortunate pitfall with this component's testing setup, | ||
| // but even though we use the value of storybookMetadata as the initial | ||
| // value of the activeMetadata, we cannot put activeMetadata itself into | ||
| // the dependency array. If we did, we would destroy and rebuild each | ||
| // connection every single time a new message comes in from the socket, | ||
| // because the socket has to be wired up to the state setter | ||
| if (storybookMetadata !== undefined) { | ||
ContributorAuthor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. This was removed because it introduces unnecessary renders – better to just use | ||
| return; | ||
| } | ||
| let timeoutId: number | undefined = undefined; | ||
| let activeSocket: OneWayWebSocket<ServerSentEvent> | null = null; | ||
| let retries = 0; | ||
| const createNewConnection = () => { | ||
ContributorAuthor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. The previous
Figured it'd be better to split up those responsibilities | ||
| const socket = watchAgentMetadata(agent.id); | ||
| activeSocket = socket; | ||
| socket.addEventListener("error", () => { | ||
| setActiveMetadata(undefined); | ||
| window.clearTimeout(timeoutId); | ||
| // The error event is supposed to fire when an error happens | ||
| // with the connection itself, which implies that the connection | ||
| // would auto-close. Couldn't find a definitive answer on MDN, | ||
| // though, so closing it manually just to be safe | ||
| socket.close(); | ||
| activeSocket = null; | ||
| retries++; | ||
| if (retries >= maxSocketErrorRetryCount) { | ||
| displayError( | ||
| "Unexpected disconnect while watching Metadata changes. Please try refreshing the page.", | ||
| ); | ||
| return; | ||
| } | ||
| displayError( | ||
| "Unexpected disconnect while watching Metadata changes. Creating new connection...", | ||
| ); | ||
| timeoutId = window.setTimeout(() => { | ||
| createNewConnection(); | ||
| }, 3_000); | ||
| }); | ||
| socket.addEventListener("message", (e) => { | ||
| if (e.parseError) { | ||
| displayError( | ||
| "Unable to process newest response from server. Please try refreshing the page.", | ||
| ); | ||
| return; | ||
| } | ||
| const msg = e.parsedMessage; | ||
| if (msg.type === "data") { | ||
| setActiveMetadata(msg.data as WorkspaceAgentMetadata[]); | ||
| } | ||
| }); | ||
| }; | ||
| createNewConnection(); | ||
| return () => { | ||
| window.clearTimeout(timeoutId); | ||
| activeSocket?.close(); | ||
| }; | ||
| }, [agent.id, storybookMetadata]); | ||
| if (activeMetadata === undefined) { | ||
| return ( | ||
| <section css={styles.root}> | ||
| <AgentMetadataSkeleton /> | ||
| </section> | ||
| ); | ||
| } | ||
| return <AgentMetadataView metadata={activeMetadata} />; | ||
| }; | ||
| export const AgentMetadataSkeleton: FC = () => { | ||
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.