RPC Docs.

Prim+RPC is prerelease software. It may be unstable and functionality may change prior to full release.

RPC Structure

This is a slightly more advanced guide for those using Prim+RPC outside of JavaScript. You do not need to read this to understand how to use Prim+RPC.

While using Prim+RPC's client to generate RPC is incredibly easy and painless, there may be situations where you would like to learn how to structure a request to a Prim+RPC server. Maybe you are developing an application with Swift/Kotlin and need to connect to Prim+RPC from an ordinary HTTP client, maybe you're looking to intercept requests with a Web Worker for caching, or maybe you are just curious as to how it works.

Before you learn how to format an RPC manually, you should also know that Prim+RPC has a low-level documentation generator for which you could generate RPC snippets for popular HTTP clients using HTTPSnippet. These can be customized for your own code being used with Prim+RPC. This is useful when you have potentially hundreds of different method calls to make. Otherwise, you may just learn how to make the request yourself since it is designed to be easy to do.

While Prim+RPC only deals with the RPC portion of the request, plugins used with Prim+RPC generally follow a particular format to deliver that RPC (although they're not technically required to do so). We'll discuss this by working with typical RPCs that may be sent in Prim+RPC.

Table of Contents

Simple RPC

Let's look at a simple function call and compare it directly against the resulting RPC. We'll define the client and server in the same file for easier reading. In a real-world scenario, we would use a Prim+RPC plugin but we'll use testing plugins for example. The focus is only on the add() function but if you'd like to test this out, you may copy the snippet below to see how it actually runs.

index.ts

import { createPrimClient, createPrimServer, testing } from "@doseofted/prim-rpc"
// define the function
function add(a: number, b: number) {
return a + b
}
// assign property "rpc" (otherwise the function isn't allowed to be called remotely)
add.rpc = true
// we'll give Prim RPC Server a module (or a variable resembling a module, like below)
const module = { add }
// create plugins for testing Prim RPC locally (real-world usage may use HTTP/WS plugins)
const { methodHandler, callbackHandler, methodPlugin, callbackPlugin } = testing.createPrimTestingPlugins()
// pass module and plugins to Prim RPC server
const backend = createPrimServer({ module, methodHandler, callbackHandler })
// create a Prim RPC client (and give plugins used to communicate with server)
const frontend = createPrimClient<typeof module>({ methodPlugin, callbackPlugin })
// now make the function call (we can compare this to the expected result, for testing)
const result = await frontend.add(1, 2)
const expected = module.add(1, 2)
console.log(result, result === expected)

The function call above, when used with Prim+RPC, would generate the following RPC request and result:

request.json

{
"method": "add",
"args": [1, 2],
"id": "..."
}

response.json

{
"id": "...",
"result": 3
}

It's that simple! If you were to make this RPC manually, you wouldn't even need to include the "id" property in the request (this is optional and is used by the Prim+RPC client to sort out batched requests). Depending on how you send/receive RPC, this result may be wrapped in some other other container, like an HTTP request, but the RPC itself is contained in this JSON.

We simply gave the method name and arguments to the method then received a result. Our arguments were given as an array because we have more than one but if we had a single argument (maybe an object), then we would simply need to pass that argument without wrapping in an array. Let's say we have function like so:

index.ts

/** Alternative version of example above. Notice that there's now a single parameter. */
function add(options: { a: number; b: number }) {
return options.a + options.b
}

When used with Prim+RPC, this would result in the following RPC:

request.json

{
"method": "add",
"args": {
"a": 1,
"b": 2
}
}

response.json

{
"result": 3
}

That's about all there is to know about making a simple function call and receiving a result. However, there are situations where you may not receive a return value back. We'll cover this situation next.

Handling an Error

Most of the time requests with Prim+RPC will return a result in its RPC contained in the .result property of the response. However, if your function throws an error, then this is no longer the case. When your function errors on the server, that error is caught, serialized, and forwarded to the client where the error is recreated and thrown for the client to handle.

Let's take our previous add() example and make a small modification:


function add(a: number, b: number) {
if (typeof a !== "number") {
throw "a is not number"
}
if (typeof b !== "number") {
throw "b is not number"
}
return a + b
}

Given valid input, the RPC result is still the same as it was before. However, if we give an argument of a string, an error is thrown and the following result is received.

index.ts

try {
add("not a number", 2)
} catch (error) {
// error === "a is not number"
}

Running this code would result in the following RPC:

request.json

{
"method": "add",
"args": {
"a": "not a number",
"b": 2
}
}

response.json

{
"error": "a is not number"
}

The error thrown on the server was serialized and sent to the client on the property .error which was then thrown again on the Prim+RPC client.

You also have the option of throwing an Error object which are serialized by default using the library serialize-error. This is the default option in Prim+RPC but can also be disabled by setting an option in Prim+RPC handleError to false.

With Callback

Prim+RPC can also support callbacks given as an argument to a function that you define. Since callbacks are called at a later point in time, the initial RPC is followed not just by a single return value but with multiple responses for each time the callback given is called from the server.

It's easiest to explain with an example. Let's take a look at the function below.


function typeMessage(message: string, typeLetter: (letter: string) => void) {
let timeout = 0
message.split("").forEach(letter => {
setTimeout(() => {
typeLetter(letter)
}, ++timeout * 150)
})
}
// example usage
typeMessage("Hi!", letter => console.log(letter))

This function takes a string message and, for each letter in the message, calls typeLetter() with a short delay between each letter. Similar to other function calls, there is an initial RPC and a response:

request.json

{
"method": "typeMessage",
"args": ["Hi!", "_cb_1234"]
}

response-1.json

{
"result": null
}

We can't serialize the callback function, so instead it is replaced with a string identifier that we will keep track of when we receive a result from the server. When the server calls the callback, we can expect the server to return this identifier back to use with the arguments used on the server when it was called. Prim+RPC's client will automatically generate the callback identifier and match it with a response from the server. When using outside of JavaScript, the request and responses (typically over a WebSocket connection) need to be matched.

Note that the return value of the function is null because the function had no return value (and undefined can't be sent back with the default JSON handler, because it's not a valid JSON value).

However, we're not done yet. The Prim+RPC server will send back additional messages as the callback is called from the server (tap each tab to see responses):

response-2.json
response-3.json
response-4.json

{
"result": ["H"],
"id": "_cb_1234"
}

Each callback result includes the identifier that we sent in the request for us to match back to the request.

With Custom JSON Handler

Up until this point, we've worked with primitive values on the function. With Prim+RPC we can override the default JSON handler and use a custom handler. It doesn't even necessarily need to be JSON.

Documentation in Progress

With File

Prim+RPC can handle files in two different ways: by supporting files in a custom JSON-like handler or by using the default JSON handler and separating files from the RPC itself (by replacing files in a request with identifiers to the file).

Documentation in Progress

Batching Requests

Prim+RPC can batch multiple function calls into a single request.

Documentation in Progress

Prim+RPC: a project by Ted Klingenberg

Dose of Ted

Anonymous analytics collected with Ackee