Files
smartvm/ts/classes.socketclient.ts
T

147 lines
4.0 KiB
TypeScript
Raw Permalink Normal View History

2026-02-08 21:47:33 +00:00
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);
}
2026-02-08 21:47:33 +00:00
/**
* HTTP client that communicates with Firecracker over a Unix domain socket.
* Uses @push.rocks/smartrequest with the `http://unix:<socket>:<path>` 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<T>(response: any): Promise<T> {
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;
}
}
2026-02-08 21:47:33 +00:00
/**
* Perform a GET request.
*/
public async get<T = any>(apiPath: string): Promise<IApiResponse<T>> {
const url = this.buildUrl(apiPath);
try {
const response = await plugins.SmartRequest.create()
.url(url)
.get();
const statusCode = response.status;
const body = await this.parseResponseBody<T>(response);
2026-02-08 21:47:33 +00:00
return {
statusCode,
body,
ok: response.ok,
};
} catch (err) {
throw new SmartVMError(
`GET ${apiPath} failed: ${getErrorMessage(err)}`,
2026-02-08 21:47:33 +00:00
'SOCKET_REQUEST_FAILED',
);
}
}
/**
* Perform a PUT request with a JSON body.
*/
public async put<T = any>(apiPath: string, body?: Record<string, any>): Promise<IApiResponse<T>> {
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));
}
2026-02-08 21:47:33 +00:00
const response = await request.put();
const statusCode = response.status;
const responseBody = await this.parseResponseBody<T>(response);
2026-02-08 21:47:33 +00:00
return {
statusCode,
body: responseBody,
ok: response.ok,
};
} catch (err) {
throw new SmartVMError(
`PUT ${apiPath} failed: ${getErrorMessage(err)}`,
2026-02-08 21:47:33 +00:00
'SOCKET_REQUEST_FAILED',
);
}
}
/**
* Perform a PATCH request with a JSON body.
*/
public async patch<T = any>(apiPath: string, body?: Record<string, any>): Promise<IApiResponse<T>> {
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));
}
2026-02-08 21:47:33 +00:00
const response = await request.patch();
const statusCode = response.status;
const responseBody = await this.parseResponseBody<T>(response);
2026-02-08 21:47:33 +00:00
return {
statusCode,
body: responseBody,
ok: response.ok,
};
} catch (err) {
throw new SmartVMError(
`PATCH ${apiPath} failed: ${getErrorMessage(err)}`,
2026-02-08 21:47:33 +00:00
'SOCKET_REQUEST_FAILED',
);
}
}
/**
* Check if the Firecracker API socket is ready by polling GET /version.
2026-02-08 21:47:33 +00:00
*/
public async isReady(timeoutMs: number = 5000): Promise<boolean> {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
try {
const response = await this.get('/version');
if (response.ok) {
2026-02-08 21:47:33 +00:00
return true;
}
} catch {
// Socket not ready yet
}
await plugins.smartdelay.delayFor(100);
}
return false;
}
}