Skip to content

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.

SDK Reference