import * as plugins from './plugins.js'; import type { ISocketClientOptions, IApiResponse } from './interfaces/index.js'; import { SmartVMError } from './interfaces/index.js'; function getErrorMessage(err: unknown): string { return err instanceof Error ? err.message : String(err); } /** * HTTP client that communicates with Firecracker over a Unix domain socket. * Uses @push.rocks/smartrequest with the `http://unix::` URL format. */ export class SocketClient { private socketPath: string; constructor(options: ISocketClientOptions) { this.socketPath = options.socketPath; } /** * Build the Unix socket URL for a given API path. */ private buildUrl(apiPath: string): string { return `http://unix:${this.socketPath}:${apiPath}`; } private async parseResponseBody(response: any): Promise { try { const text = await response.text(); if (!text) { return undefined as T; } try { return JSON.parse(text) as T; } catch { return text as T; } } catch { return undefined as T; } } /** * Perform a GET request. */ public async get(apiPath: string): Promise> { const url = this.buildUrl(apiPath); try { const response = await plugins.SmartRequest.create() .url(url) .get(); const statusCode = response.status; const body = await this.parseResponseBody(response); return { statusCode, body, ok: response.ok, }; } catch (err) { throw new SmartVMError( `GET ${apiPath} failed: ${getErrorMessage(err)}`, 'SOCKET_REQUEST_FAILED', ); } } /** * Perform a PUT request with a JSON body. */ public async put(apiPath: string, body?: Record): Promise> { const url = this.buildUrl(apiPath); try { let request = plugins.SmartRequest.create().url(url); if (body !== undefined) { const bodyBuffer = Buffer.from(JSON.stringify(body)); request = request .buffer(bodyBuffer, 'application/json') .header('Content-Length', String(bodyBuffer.length)); } const response = await request.put(); const statusCode = response.status; const responseBody = await this.parseResponseBody(response); return { statusCode, body: responseBody, ok: response.ok, }; } catch (err) { throw new SmartVMError( `PUT ${apiPath} failed: ${getErrorMessage(err)}`, 'SOCKET_REQUEST_FAILED', ); } } /** * Perform a PATCH request with a JSON body. */ public async patch(apiPath: string, body?: Record): Promise> { const url = this.buildUrl(apiPath); try { let request = plugins.SmartRequest.create().url(url); if (body !== undefined) { const bodyBuffer = Buffer.from(JSON.stringify(body)); request = request .buffer(bodyBuffer, 'application/json') .header('Content-Length', String(bodyBuffer.length)); } const response = await request.patch(); const statusCode = response.status; const responseBody = await this.parseResponseBody(response); return { statusCode, body: responseBody, ok: response.ok, }; } catch (err) { throw new SmartVMError( `PATCH ${apiPath} failed: ${getErrorMessage(err)}`, 'SOCKET_REQUEST_FAILED', ); } } /** * Check if the Firecracker API socket is ready by polling GET /version. */ public async isReady(timeoutMs: number = 5000): Promise { const start = Date.now(); while (Date.now() - start < timeoutMs) { try { const response = await this.get('/version'); if (response.ok) { return true; } } catch { // Socket not ready yet } await plugins.smartdelay.delayFor(100); } return false; } }