import WebSocket from "ws"; import { GenericMiddleware, ISocketClient, MethodHandlers, RequestContextBase, SocketTimeouts, } from "../types"; import { ClientMethods, createRequestHandlers, ServerMethods, } from "./handlers"; import { createISocketClient, ISocket } from "./socket"; export type StdISocketRPCClient = ISocketClient; export async function connectToISocketRPCServer({ superblocksBaseUrl, agentUrl, token, }: { superblocksBaseUrl: string; token: string; agentUrl?: string; }): Promise { const requestHandlers = createRequestHandlers({ agentUrl, token, }); const authorization = `Bearer ${token}`; const wsUrl = new URL("api/v1/rpc-ws", superblocksBaseUrl); if (wsUrl.protocol === "http:") { wsUrl.protocol = "ws:"; } else if (wsUrl.protocol === "https:") { wsUrl.protocol = "wss:"; } if (wsUrl.host === "localhost:3000") { wsUrl.host = "127.0.0.1:8080"; } else if (wsUrl.hostname === "localhost") { wsUrl.hostname = "127.0.0.1"; } return await connectISocket( wsUrl.href, authorization, requestHandlers, [], { connectionTimeoutInSeconds: 6 * 60, // 6 minutes noResponseTimeoutInSeconds: 5 * 60, // 5 minutes }, ); } // a subclass of ISocket that sends an auth token on the first request // this is useful for client-side sockets that need to authenticate // TODO(george): if we start using this for long-lived connections, we should add a way to refresh the token export class ISocketWithClientAuth< ImplementedMethods, CallableMethods, RequestContext extends RequestContextBase, > extends ISocket { private readonly authorization?: string; private hasSentAuth = false; constructor( ws: WebSocket, authorization: string | undefined, requestHandlers: MethodHandlers< ImplementedMethods, CallableMethods, RequestContext >, globalMiddlewares: GenericMiddleware[], timeouts?: SocketTimeouts, ) { super(ws, requestHandlers, globalMiddlewares, timeouts); this.authorization = authorization; } // override `request` from the base class to send `authorization` when appropriate async request( method: string, params: Params, ): Promise { // only send `authorization` on the first request const authorization = this.hasSentAuth ? undefined : this.authorization; const result = await super.request( method, params, authorization, ); this.hasSentAuth = true; return result; } } export async function connectISocket< CallableMethods, ImplementedMethods, RequestContext extends RequestContextBase = RequestContextBase, >( wsUrl: string, authorization: string | undefined, requestHandlers: MethodHandlers< ImplementedMethods, CallableMethods, RequestContext >, globalMiddlewares: GenericMiddleware[], timeouts?: SocketTimeouts, ): Promise> { const ws = await connectWebSocket(wsUrl); const isocket = new ISocketWithClientAuth( ws, authorization, requestHandlers, globalMiddlewares, timeouts, ); return createISocketClient(isocket); } export function connectWebSocket(wsUrl: string): Promise { return new Promise((resolve, reject) => { const ws = new WebSocket(wsUrl); ws.addEventListener("open", () => { // Resolve the promise with the WebSocket instance when the connection is open resolve(ws); }); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore ws.addEventListener("error", (error: Error) => { // Reject the promise if there's an error reject(error); }); }); }