5. RPC API

The RPC (Remote Procedure Call) API provides a mechanism for making structured function calls over WebSocket connections using the Message API. It builds on top of the messaging layer to provide a request-response pattern with automatic message tagging, retry logic, and a modular server-side dispatch system. The RPC API has both a client-side component for making calls and a server-side component for servicing them.

5.1. Client-Side RPC

The client-side RPC API is encapsulated in the vrtql_rpc structure. This structure wraps a WebSocket connection (vws_cnx) and provides facilities for making RPC calls, handling timeouts with automatic retries, processing out-of-band messages, and reconnecting on connection failure.

You create an RPC instance using vrtql_rpc_new(), passing in an established WebSocket connection. The RPC instance does not take ownership of the connection — you are responsible for freeing both the RPC instance and the connection independently. The instance is configured with sensible defaults: five retry attempts on timeout and a default out-of-band message handler that simply discards unrecognized messages.

There are two ways to make RPC calls. The first is vrtql_rpc_exec(), which is the low-level call. It takes a vrtql_msg request, sends it to the server, and returns the raw reply message. The caller takes ownership of the reply and must free it with vrtql_msg_free(). This function automatically generates a random tag and sets it in the message's routing map under the key "tag". It uses this tag to correlate the response with the request. Any messages received that do not match the tag are passed to the out_of_band callback handler.

The second method is vrtql_rpc_invoke(), which is a higher-level convenience function. It calls vrtql_rpc_exec() internally but additionally translates the response's "rc" (return code) and "msg" (message) headers into the thread-local error state (vws.e.code and vws.e.text). If the response contains content, it is copied into the rpc->val buffer. This function takes ownership of the request message — the caller should not use or free the request after calling it. It returns true if a response was received and false otherwise.

Both functions handle connection failures gracefully. If the connection drops during a send, the RPC instance will attempt to reconnect using the vws_reconnect() function and, if a reconnect callback is defined, will invoke it to allow the caller to perform any post-reconnection setup (such as re-authentication). If reconnection succeeds, the send is retried. If reconnection fails or the connection drops during a receive, the error state is set with VE_SOCKET along with VE_SEND or VE_RECV to indicate where the failure occurred.

You can also generate random tags independently using vrtql_rpc_tag(), which produces a random alphanumeric string of the specified length. The caller is responsible for freeing the returned string.

The following example illustrates basic client-side RPC usage:

// Create a WebSocket connection and connect
vws_cnx* cnx = vws_cnx_new();
vws_connect(cnx, "ws://localhost:8181/websocket");

// Create an RPC instance using the connection
vrtql_rpc* rpc = vrtql_rpc_new(cnx);

// Create a request message
vrtql_msg* req = vrtql_msg_new();
vrtql_msg_set_header(req, "id", "session.login");
vrtql_msg_set_header(req, "username", "admin");
vrtql_msg_set_header(req, "password", "secret");

// Make the RPC call (low-level)
vrtql_msg* reply = vrtql_rpc_exec(rpc, req);

if (reply != NULL)
{
    cstr rc = vrtql_msg_get_header(reply, "rc");
    printf("Return code: %s\n", rc);
    vrtql_msg_free(reply);
}

// Cleanup
vrtql_msg_free(req);
vrtql_rpc_free(rpc);
vws_disconnect(cnx);
vws_cnx_free(cnx);

5.2. Server-Side RPC

The server-side RPC API provides a modular dispatch system for organizing and servicing RPC calls. It is built around three key structures: modules, a system registry, and an execution environment.

An RPC module (vrtql_rpc_module) is a named collection of RPC functions. You create a module using vrtql_rpc_module_new(), passing in the module name. You then register individual RPC functions within the module using vrtql_rpc_module_set(). Each function is identified by a name and must conform to the vrtql_rpc_call callback signature, which takes a vrtql_rpc_env pointer and a vrtql_msg pointer and returns a reply message. You can look up registered functions using vrtql_rpc_module_get().

The RPC system (vrtql_rpc_system) is a registry that holds modules. You create a system using vrtql_rpc_system_new() and register modules in it using vrtql_rpc_system_set(). You can retrieve modules by name using vrtql_rpc_system_get().

The execution environment (vrtql_rpc_env) provides context to RPC handlers during execution. It contains a data pointer for user-defined data and a module pointer that references the module containing the invoked function. This environment is set up by the dispatch system before calling the handler.

To service an incoming RPC call, you use vrtql_rpc_service(). This function takes the RPC system, an environment, and the incoming request message. The request must contain an "id" header in the format "module.function" (for example, "session.login"). The service function parses this identifier, looks up the appropriate module and function, sets the environment, and invokes the handler. It takes ownership of the request message and frees it after the handler returns. The handler's reply message is returned to the caller.

To assist in writing RPC handlers, the vrtql_rpc_reply() convenience function creates a new reply message, copies the request's tag into the reply for correlation, and sets the reply format to match the request.

The following example illustrates setting up a server-side RPC system:

// Define RPC handler functions
vrtql_msg* session_login(vrtql_rpc_env* e, vrtql_msg* req)
{
    vrtql_msg* reply = vrtql_rpc_reply(req);
    vrtql_msg_set_header(reply, "rc", "0");
    vrtql_msg_set_header(reply, "msg", "Login successful");
    return reply;
}

vrtql_msg* session_logout(vrtql_rpc_env* e, vrtql_msg* req)
{
    vrtql_msg* reply = vrtql_rpc_reply(req);
    vrtql_msg_set_header(reply, "rc", "0");
    return reply;
}

// Create and populate a module
vrtql_rpc_module* module = vrtql_rpc_module_new("session");
vrtql_rpc_module_set(module, "login",  session_login);
vrtql_rpc_module_set(module, "logout", session_logout);

// Create the system and register the module
vrtql_rpc_system* system = vrtql_rpc_system_new();
vrtql_rpc_system_set(system, module);

// Service an incoming request (e.g. from a message server handler)
vrtql_rpc_env env;
env.data = NULL;

vrtql_msg* req = vrtql_msg_new();
vrtql_msg_set_header(req, "id", "session.login");

vrtql_msg* reply = vrtql_rpc_service(system, &env, req);

// reply now contains the handler's response
// req has been freed by vrtql_rpc_service()

if (reply != NULL)
{
    vrtql_msg_free(reply);
}

// Cleanup
vrtql_rpc_system_free(system);