diff --git a/enclave-manager/web/packages/app/src/client/enclaveManager/AuthenticatedKurtosisClient.ts b/enclave-manager/web/packages/app/src/client/enclaveManager/AuthenticatedKurtosisClient.ts index 00283aa969..6966a28357 100644 --- a/enclave-manager/web/packages/app/src/client/enclaveManager/AuthenticatedKurtosisClient.ts +++ b/enclave-manager/web/packages/app/src/client/enclaveManager/AuthenticatedKurtosisClient.ts @@ -11,7 +11,7 @@ import { KURTOSIS_DEFAULT_REST_API_PORT, } from "../constants"; import { KurtosisClient } from "./KurtosisClient"; -import createWSClient from "./websocketClient/WebSocketClient"; +import { createWSClient } from "./websocketClient/WebSocketClient"; function constructGatewayURL(remoteHost: string): string { return `${KURTOSIS_CLOUD_UI_URL}/gateway/ips/${remoteHost}/ports/${KURTOSIS_DEFAULT_EM_API_PORT}`; @@ -22,7 +22,7 @@ function constructRESTGatewayURL(remoteHost: string): string { } function constructWSGatewayURL(remoteHost: string): string { - return constructRESTGatewayURL(remoteHost).replace(/^http/, 'ws'); + return constructRESTGatewayURL(remoteHost).replace(/^http/, "ws"); } export class AuthenticatedKurtosisClient extends KurtosisClient { diff --git a/enclave-manager/web/packages/app/src/client/enclaveManager/KurtosisClient.ts b/enclave-manager/web/packages/app/src/client/enclaveManager/KurtosisClient.ts index b2efa82e0f..1567967a1b 100644 --- a/enclave-manager/web/packages/app/src/client/enclaveManager/KurtosisClient.ts +++ b/enclave-manager/web/packages/app/src/client/enclaveManager/KurtosisClient.ts @@ -27,7 +27,7 @@ import { components, paths } from "kurtosis-sdk/src/engine/rest_api_bindings/typ import { assertDefined, asyncResult, isDefined, RemoveFunctions } from "kurtosis-ui-components"; import createClient from "openapi-fetch"; import { EnclaveFullInfo } from "../../emui/enclaves/types"; -import createWSClient from "./websocketClient/WebSocketClient"; +import { createWSClient } from "./websocketClient/WebSocketClient"; type KurtosisRestClient = ReturnType>; type KurtosisWebsocketClient = ReturnType>; diff --git a/enclave-manager/web/packages/app/src/client/enclaveManager/LocalKurtosisClient.ts b/enclave-manager/web/packages/app/src/client/enclaveManager/LocalKurtosisClient.ts index 888559f8e3..ced3d9bbb9 100644 --- a/enclave-manager/web/packages/app/src/client/enclaveManager/LocalKurtosisClient.ts +++ b/enclave-manager/web/packages/app/src/client/enclaveManager/LocalKurtosisClient.ts @@ -3,9 +3,13 @@ import { createConnectTransport } from "@connectrpc/connect-web"; import { KurtosisEnclaveManagerServer } from "enclave-manager-sdk/build/kurtosis_enclave_manager_api_connect"; import { paths } from "kurtosis-sdk/src/engine/rest_api_bindings/types"; import createClient from "openapi-fetch"; -import { KURTOSIS_EM_API_DEFAULT_URL, KURTOSIS_REST_API_DEFAULT_URL, KURTOSIS_WEBSOCKET_API_DEFAULT_URL } from "../constants"; +import { + KURTOSIS_EM_API_DEFAULT_URL, + KURTOSIS_REST_API_DEFAULT_URL, + KURTOSIS_WEBSOCKET_API_DEFAULT_URL, +} from "../constants"; import { KurtosisClient } from "./KurtosisClient"; -import createWSClient from "./websocketClient/WebSocketClient"; +import { createWSClient } from "./websocketClient/WebSocketClient"; export class LocalKurtosisClient extends KurtosisClient { constructor() { diff --git a/enclave-manager/web/packages/app/src/client/enclaveManager/websocketClient/WebSocketClient.d.ts b/enclave-manager/web/packages/app/src/client/enclaveManager/websocketClient/WebSocketClient.d.ts deleted file mode 100644 index 1421bac143..0000000000 --- a/enclave-manager/web/packages/app/src/client/enclaveManager/websocketClient/WebSocketClient.d.ts +++ /dev/null @@ -1,43 +0,0 @@ -import type { - ErrorResponse, - SuccessResponse, - FilterKeys, - MediaType, - PathsWithMethod, - ResponseObjectMap, - OperationRequestBodyContent, - HasRequiredKeys, -} from "openapi-typescript-helpers"; - -import type { - FetchResponse, - ParamsOption, - QuerySerializer -} from "openapi-fetch" - -export interface ClientOptions { - baseUrl?: string; -} - -export type RequestOptions = ParamsOption & { - querySerializer?: QuerySerializer; - abortSignal?: AbortSignal; -}; - -// This implementation is based on the http version of the lib `openapi-fetch` -// https://github.com/drwpow/openapi-typescript/blob/main/packages/openapi-fetch/src/index.d.ts -export default function createWSClient( - clientOptions?: ClientOptions, -): { - /** Call a WS endpoint */ - WS

>( - url: P, - ...init: HasRequiredKeys< - RequestOptions> - > extends never - ? [(RequestOptions> | undefined)?] - : [RequestOptions>] - ): AsyncGenerator>; -}; - -export { }; diff --git a/enclave-manager/web/packages/app/src/client/enclaveManager/websocketClient/WebSocketClient.js b/enclave-manager/web/packages/app/src/client/enclaveManager/websocketClient/WebSocketClient.js deleted file mode 100644 index 359341b4f4..0000000000 --- a/enclave-manager/web/packages/app/src/client/enclaveManager/websocketClient/WebSocketClient.js +++ /dev/null @@ -1,69 +0,0 @@ -import { createFinalURL, defaultQuerySerializer } from "openapi-fetch"; - -export default function createWSClient(clientOptions) { - const { baseUrl = "" } = clientOptions ?? {}; - if (baseUrl.endsWith("/")) { - baseUrl = baseUrl.slice(0, -1); // remove trailing slash - } - - async function* websocketMessagesGenerator(url, fetchOptions) { - const { params = {}, querySerializer = defaultQuerySerializer, abortSignal, ...init } = fetchOptions || {}; - - // build full URL - const finalURL = createFinalURL(url, { - baseUrl, - params, - querySerializer, - }); - - var socket; - try { - socket = new WebSocket(finalURL); - } catch (error) { - return { error: {}, data: null }; - } - - // Wait for the WebSocket connection to be open - await new Promise((resolve) => { - socket.addEventListener("open", resolve); - }); - - if (abortSignal) { - // already aborted, fail immediately - if (abortSignal.aborted) { - console.warn(`Websocket on ${finalURL} got aborted before using. Closing it.`); - socket.close(); - } - - // close later if aborted - abortSignal.addEventListener("abort", () => { - console.warn(`Websocket on ${finalURL} has been asked to abort. Closing it.`); - socket.close(); - }); - } - - try { - while (socket.readyState === WebSocket.OPEN) { - // Wait for the next message - const message = await new Promise((resolve) => { - socket.addEventListener("message", (event) => resolve(event)); - }); - - // Yield the received message - yield { error: undefined, data: JSON.parse(message.data) }; - } - } finally { - // Close the WebSocket connection when the generator is done - socket.close(); - } - } - - return { - /** Call a WS endpoint */ - WS: async function* (url, init) { - for await (const val of websocketMessagesGenerator(url, { ...init, method: "GET" })) { - yield val; - } - }, - }; -} diff --git a/enclave-manager/web/packages/app/src/client/enclaveManager/websocketClient/WebSocketClient.ts b/enclave-manager/web/packages/app/src/client/enclaveManager/websocketClient/WebSocketClient.ts new file mode 100644 index 0000000000..00c329e308 --- /dev/null +++ b/enclave-manager/web/packages/app/src/client/enclaveManager/websocketClient/WebSocketClient.ts @@ -0,0 +1,119 @@ +import type { + ErrorResponse, + FilterKeys, + MediaType, + PathsWithMethod, + ResponseObjectMap, + SuccessResponse, +} from "openapi-typescript-helpers"; + +import type { ParamsOption, QuerySerializer } from "openapi-fetch"; + +import { createFinalURL, defaultQuerySerializer } from "openapi-fetch"; + +export interface ClientOptions { + baseUrl?: string; +} + +export type RequestOptions = ParamsOption & { + querySerializer?: QuerySerializer; + abortSignal?: AbortSignal; +}; + +type ReturnType> = "get" extends infer T + ? T extends "get" + ? T extends keyof Paths[P] + ? Paths[P][T] + : unknown + : never + : never; + +type ParamsType> = RequestOptions< + FilterKeys +>; + +export type MessageResponse = + | { + data: FilterKeys>, MediaType>; + error?: never; + message: MessageEvent; + } + | { + data?: never; + error: FilterKeys>, MediaType>; + message: MessageEvent; + }; + +// This implementation is based on the http version of the lib `openapi-fetch` +// https://github.com/drwpow/openapi-typescript/blob/main/packages/openapi-fetch/src/index.d.ts +export function createWSClient( + clientOptions?: ClientOptions, +): { + WS:

, T extends keyof Paths[P]>( + url: P, + ...init: ParamsType[] + ) => AsyncGenerator>>; +} { + var baseUrl = clientOptions?.baseUrl ?? ""; + if (baseUrl.endsWith("/")) { + baseUrl = baseUrl.slice(0, -1); // remove trailing slash + } + + return { + /** Call a WS endpoint */ + WS: async function* (url, fetchOptions) { + const { params = {}, querySerializer = defaultQuerySerializer, abortSignal, ...init } = fetchOptions || {}; + + // build full URL + const finalURL = createFinalURL(url.toString(), { + baseUrl, + params, + querySerializer, + }); + + var socket: WebSocket; + try { + socket = new WebSocket(finalURL); + } catch (error) { + return { error: {}, data: null }; + } + + // Wait for the WebSocket connection to be open + await new Promise((resolve) => { + socket.addEventListener("open", resolve); + }); + + if (abortSignal) { + // already aborted, fail immediately + if (abortSignal.aborted) { + console.warn(`Websocket on ${finalURL} got aborted before using. Closing it.`); + socket.close(); + } + + // close later if aborted + abortSignal.addEventListener("abort", () => { + console.warn(`Websocket on ${finalURL} has been asked to abort. Closing it.`); + socket.close(); + }); + } + + try { + while (socket.readyState === WebSocket.OPEN) { + // Wait for the next message + const message: MessageEvent = await new Promise((resolve) => { + socket.addEventListener("message", (event: MessageEvent) => resolve(event)); + }); + + // Yield the received message + yield { error: undefined, data: JSON.parse(message.data), message: message }; + } + } catch (error) { + console.error(`Received an unexpected message from the channel on ${finalURL}:`); + console.error(error); + } finally { + // Close the WebSocket connection when the generator is done + socket.close(); + } + }, + }; +}