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, cloudevent: CloudEvent): Promise<PublishResponse>
publishWithResult(topic: string, cloudevent: CloudEvent, replyTopic: string, opts?: {
timeout?: number;
correlationId?: string;
}): Promise<SubscribeResponse>
subscribe(services: string[], topics: string[], eventTypes: string[]): AsyncIterable<TopicEventRequest>
Example — Publish:
import { create } from "@bufbuild/protobuf";
import { CloudEventSchema } from "./generated/cloudevents_pb.js";
const cloudevent = create(CloudEventSchema, {
id: crypto.randomUUID(),
source: "urn:my-app",
type: "my-event",
data: {
textData: JSON.stringify({ key: "value" }),
},
});
const response = await client.publish("my-topic", cloudevent);
console.log(`Published: ${response.status?.success}`);
Example — Request-Reply:
import { create } from "@bufbuild/protobuf";
import { CloudEventSchema } from "./generated/cloudevents_pb.js";
const cloudevent = create(CloudEventSchema, {
id: crypto.randomUUID(),
source: "urn:my-app",
type: "my-command",
data: {
textData: JSON.stringify({ command: "status" }),
},
});
const response = await client.publishWithResult(
"worker-commands",
cloudevent,
"worker-responses",
{ timeout: 10000 }
);
const decoder = new TextDecoder();
console.log(`Response: ${decoder.decode(response.cloudevent!.data!.textData)}`);
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)}`);
}
Note on broadcast semantics: Pubsub.Subscribe is a per-API-process broadcast subscription — multiple in-process callers on the same topic all receive every message (per the Pub/Sub Topics spec §Subscription Implementation Pattern). Gateway.Subscribe is also broadcast within the same API process. If you need competing-consumer semantics, configure your Dapr component to use a consumer group (pubsub.kafka with a group, pubsub.rabbitmq with a queue).
Resource usage: publishWithResult shares one persistent gRPC pubsub.subscribe stream per reply topic across all callers (started lazily on first use, reconnecting with backoff if it drops) and demuxes responses by correlationid. The underlying Dapr subscription is created once and stays warm; per-call cost is one pending waiter entry. Readers are cancelled by close().
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 deserializes 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.