TypeScript Client Library
The Virtufin API TypeScript client library (virtufin-api) provides a high-level TypeScript interface for interacting with the Virtufin API gateway using gRPC-web.
Installation
From Gitea npm Registry
Configure authentication with a Gitea personal access token (scope: read:packages):
echo "//npm.haenerconsulting.com/api/packages/virtufin/npm/:_authToken=<your-gitea-token>" >> ~/.npmrc
Then install:
npm install virtufin-api
From Local Source
{
"dependencies": {
"virtufin-api": "file:path/to/src/typescript"
}
}
Overview
The client library provides the ApiClient class as the main entry point for all operations:
| Class | Description |
|---|---|
ApiClient |
Main entry point with async operations using Connect protocol |
Quick Start
import { ApiClient } from "virtufin-api";
const client = new ApiClient({ url: "http://localhost:5001" });
// List available services
const services = await client.listServices();
console.log(`Services: ${services.services.join(", ")}`);
// Invoke a method with binary data
const encoder = new TextEncoder();
const requestData = encoder.encode(JSON.stringify({}));
const response = await client.invoke("workmanager", "ListWorkers", requestData);
// Or with JSON string directly
const jsonResponse = await client.invokeJson("workmanager", "ListWorkers", "{}");
ApiClient
The main client class providing async operations for service discovery, method invocation, and pub/sub.
Constructor
const client = new ApiClient({ url: string, timeout?: number });
Parameters:
- url - The gateway base URL (e.g., "http://localhost:5001")
- timeout - Request timeout in milliseconds (default: 30000)
Methods
Service Discovery
listServices(): Promise<ListServicesResponse>
listMethods(service: string): Promise<ListMethodsResponse>
getMethodSchema(service: string, method: string, type: string): Promise<GetMethodSchemaResponse>
Example:
// Get all services
const services = await client.listServices();
console.log(`Services: ${services.services}`);
// Get methods for a service
const methods = await client.listMethods("workmanager");
for (const method of methods.methods) {
console.log(`Method: ${method.name}`);
console.log(` Input: ${method.inputType}`);
console.log(` Output: ${method.outputType}`);
console.log(` Streaming: ${method.isServerStreaming}`);
}
// Get method schema
const schema = await client.getMethodSchema("workmanager", "ListWorkers", "protobuf");
Method Invocation
invoke(service: string, method: string, requestData: Uint8Array): Promise<InvokeResponse>
invokeJson(service: string, method: string, requestData: string): Promise<InvokeJsonResponse>
Example:
// Simple invocation with Uint8Array
const encoder = new TextEncoder();
const requestData = encoder.encode("");
const response = await client.invoke("workmanager", "ListWorkers", requestData);
// With JSON request data
const response = await client.invokeJson("workmanager", "CreateWorker", JSON.stringify({
code_source: { url: "https://example.com/worker.py" },
mime_type: "text/x-python",
topic: "worker-commands",
}));
// Decode response
const decoder = new TextDecoder();
const responseData = decoder.decode(response.responseData);
console.log(`Response: ${responseData}`);
Pub/Sub Operations
publish(topic: string, data: Uint8Array, metadata?: Record<string, string>): Promise<PublishResponse>
publishWithResult(topic: string, data: Uint8Array, replyTopic: string, opts?: {
timeout?: number;
metadata?: Record<string, string>;
correlationId?: string;
}): Promise<PubsubSubscribeResponse>
subscribe(services: string[], topics: string[], eventTypes: string[]): AsyncIterable<TopicEventRequest>
Example — Publish:
const encoder = new TextEncoder();
const response = await client.publish("my-topic", encoder.encode(JSON.stringify({ key: "value" })));
console.log(`Published: ${response.status?.success}`);
Example — Request-Reply:
const encoder = new TextEncoder();
const response = await client.publishWithResult(
"worker-commands",
encoder.encode(JSON.stringify({ command: "status" })),
"worker-responses",
{ timeout: 10000 }
);
const decoder = new TextDecoder();
console.log(`Response: ${decoder.decode(response.data)}`);
Example — Subscribe:
const events = client.subscribe(["workmanager"], ["updates"], ["created"]);
for await (const event of events) {
console.log(`Event: ${event.service}/${event.topic}`);
const decoder = new TextDecoder();
console.log(`Data: ${decoder.decode(event.data)}`);
}
Resource Management
The client uses gRPC-web transport which manages connections automatically. No explicit close/dispose methods are required.
Dynamic Dispatch
The TypeScript client uses string-based method dispatch (no client.gateway.<service>.<method>() syntax — that pattern is only available in C# and Python). Pass the service name, method name, and JSON-serialized request as strings. The Gateway's InvokeJson RPC unserializes the JSON, runs the backend RPC, and returns a JSON-serialized response (the responseData field of InvokeJsonResponse).
The responseData field is a JSON string and must be parsed before use:
const resp = await client.invokeJson("workmanager", "ListWorkers", "{}");
const data = JSON.parse(resp.responseData ?? "{}");
// data.workers is the array of WorkerInfo messages
WorkManager
import { ApiClient } from "virtufin-api";
const client = new ApiClient({ url: "http://localhost:5001" });
// ListWorkers — no request fields
const workersResp = await client.invokeJson("workmanager", "ListWorkers", "{}");
const workers = JSON.parse(workersResp.responseData ?? "{}").workers ?? [];
for (const w of workers) {
console.log(` ${w.id}: ${w.status} (${w.language})`);
}
// CreateWorker — CodeSource oneof (url | content), mime_type, topic
const createResp = await client.invokeJson("workmanager", "CreateWorker",
JSON.stringify({
code_source: { url: "https://example.com/worker.py" },
mime_type: "text/x-python",
topic: "worker-commands",
}),
);
const workerId = JSON.parse(createResp.responseData ?? "{}").id;
await client.invokeJson("workmanager", "StartWorker",
JSON.stringify({ id: workerId }),
);
WebSocketManager
// List — no request fields
const connsResp = await client.invokeJson("websocketmanager", "List", "{}");
const conns = JSON.parse(connsResp.responseData ?? "{}").connections ?? [];
for (const c of conns) {
console.log(` ${c.id}: ${c.url} (${c.status})`);
}
// Connect — url, auto_reconnect
const connectResp = await client.invokeJson("websocketmanager", "Connect",
JSON.stringify({
url: "wss://stream.example.com/feed",
auto_reconnect: true,
}),
);
const connectionId = JSON.parse(connectResp.responseData ?? "{}").id;
await client.invokeJson("websocketmanager", "Disconnect",
JSON.stringify({ id: connectionId }),
);
Note: C# and Python expose the client.Gateway.<service>.<method>(...) /
client.gateway.<service>.<method>(...) syntax. The TypeScript wrapper
deliberately uses string-based dispatch (see
proto-to-client-mapping spec
§Known Gaps #1) — the @connectrpc/connect transport doesn't have a clean
equivalent of DynamicObject / __getattr__ for runtime method resolution.
Generated Protos
The client includes generated protobuf classes from gateway_pb:
| Type | Description |
|---|---|
InvokeRequest |
Request for Invoke RPC |
InvokeResponse |
Response from Invoke RPC |
InvokeJsonRequest |
Request for JSON invocation |
InvokeJsonResponse |
Response from JSON invocation |
ListServicesRequest |
Request to list services |
ListServicesResponse |
List of available services |
ListMethodsRequest |
Request to list methods |
ListMethodsResponse |
List of methods for a service |
GetMethodSchemaRequest |
Request for method schema |
GetMethodSchemaResponse |
Schema details |
SubscribeRequest |
Subscribe to topics |
TopicEventRequest |
Event from subscription |
Example using generated protos:
import * as protos from "./generated/gateway_pb.js";
import { Gateway } from "./generated/gateway_connect.js";
// Create a custom request
const req = new protos.InvokeRequest({
service: "workmanager",
method: "ListWorkers",
requestData: new Uint8Array()
});
Error Handling
Invocation Errors
When method invocation fails, the client throws a ConnectError:
try {
const response = await client.invoke("workmanager", "UnknownMethod", new Uint8Array());
} catch (error) {
if (error instanceof ConnectError) {
console.log(`Invocation failed: ${error.message}`);
console.log(`Code: ${error.code}`);
}
}
Connection Errors
Connection failures throw standard network errors:
try {
const client = new ApiClient({ url: "http://invalid-host:5001" });
const services = await client.listServices();
} catch (error) {
console.log(`Connection failed: ${error.message}`);
}
Complete Example
import { ApiClient } from "virtufin-api";
async function main() {
console.log("Virtufin API Client Demo");
console.log("========================\n");
const client = new ApiClient({ url: "http://localhost:5001" });
// Discover services
console.log("Available Services:");
const servicesResponse = await client.listServices();
for (const service of servicesResponse.services) {
console.log(` - ${service}`);
// List methods for each service
const methodsResponse = await client.listMethods(service);
for (const method of methodsResponse.methods.slice(0, 3)) {
const streaming = method.isServerStreaming ? " (stream)" : "";
console.log(` ${method.name}${streaming}`);
}
if (methodsResponse.methods.length > 3) {
console.log(` ... and ${methodsResponse.methods.length - 3} more`);
}
}
console.log();
// Invoke a method
console.log("Invoking workmanager.ListWorkers:");
try {
const encoder = new TextEncoder();
const response = await client.invoke("workmanager", "ListWorkers", encoder.encode(""));
const decoder = new TextDecoder();
console.log(`Response: ${decoder.decode(response.responseData)}`);
} catch (error) {
console.log(`Error: ${error}`);
}
}
main();
Protobuf Dependencies
The client library requires peer dependency:
{
"peerDependencies": {
"@bufbuild/protobuf": "^1.10.0"
}
}
Ensure your project installs the peer dependency:
npm install @bufbuild/protobuf
The proto files (gateway.proto, config.proto) are pre-compiled into the src/generated directory.