RPC Docs.

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

Configuration Reference

Prim+RPC includes two functions for configuring Prim+RPC: one for the client (createPrimClient()) and another for the server (createPrimServer()). Both of these functions accept options and are given in this reference guide.

This guide is split up into two sections: client options and server options. The Prim+RPC client expects client options only while the Prim+RPC server accepts both client and server options.

The Prim+RPC server shares options with the client for two reasons. The first: some options should be shared with both the server and client. For instance, if the JSON handler is changed on the client then it should also be changed on the server for proper serialization. The second: if a function doesn't exist on module provided to Prim+RPC server then the Prim+RPC server will create a new Prim+RPC client to contact another server (this is totally optional and allows communication between multiple servers with dedicated responsibilities).

Some options must be set to the same value on both the server and the client. To make clear which properties must be synced between client/server, you'll find Sync Required for each option below that will be either: 🟢 No meaning that the option can differ, or 🔴 Yes meaning that option must be the same.

Table of Contents

Client

The following options are available to the createPrimClient:


import { createPrimClient } from "@doseofted/prim-rpc"
import { createMethodPlugin, createCallbackPlugin } from "@doseofted/prim-rpc-plugins/{CHOOSE_YOUR_PLUGIN}"
// Tap option name below to see documentation
// You do not need to provide all of these options. This is just a demonstration.
const client = createPrimClient({
methodPlugin: createMethodPlugin(),
callbackPlugin: createCallbackPlugin(),
jsonHandler: JSON,
handleError: true,
handleBlobs: true,
endpoint: "http://example.localhost/prim",
wsEndpoint: "ws://example.localhost/prim",
clientBatchTime: 15,
})
// NOTE: all options above are also available on the server

.methodPlugin

RequiredDefaultSync Required
false(): { error: string, id: string }🟢 No

Prim+RPC is designed to be flexible and work with the frameworks that you're already using. By default, Prim+RPC can generate RPCs based on your method calls but does not include a way to send that RPC. This is handled through the usage of plugins.

Specifically, the "method" plugin is used to generate a request and parse the response given from the Prim+RPC server. For example, the "Browser API" plugin includes a method plugin that uses the web browser's Fetch API to send and receive RPC.

You can either use a pre-built plugin (included with Prim+RPC) or you can build your own. Below is an example of how to register an existing method plugin with the Prim+RPC client.


import { createPrimClient } from "@doseofted/prim-rpc"
import { createMethodPlugin } from "@doseofted/prim-rpc-plugins/browser-fetch"
const methodPlugin = createMethodPlugin()
createPrimClient({ methodPlugin })

Each method plugin may (or may not) include its own options specific to that plugin. You can find a list of available method plugins on this documentation website or you can follow this guide to create your own.

Once your method plugin is registered with the Prim+RPC client then you will also need to specify a compatible method "handler" on the Prim+RPC server. In general, a "plugin" in Prim+RPC is used to send/receive RPC on a client while a "handler" (sometimes called a "handler plugin") is used by the Prim+RPC server to handle RPC and send the RPC result back to the client.

One limitation of the "method" plugin is that it is intended to work with a single request (consisting of one or multiple RPCs) and a single response (also consisting of one/multiple RPCs). This means that the method plugin is not designed to work with callbacks because they require an open connection where multiple responses can be sent back (one response being the function result, the others as a result of a callback). If your functions use callbacks or you expect multiple responses, you should also set up the .callbackPlugin.

.callbackPlugin

RequiredDefaultSync Required
false(): { send: ((): { error: string, id: string }) }🟢 No

The "callback" plugin is very similar to the "method" plugin in the fact that it creates a request with the given RPC and parses a response from the Prim+RPC server. The primary difference between the two plugins is that the callback plugin keeps an open connection with the server to receive multiple responses. This is required for callbacks since your function call may return a value and you expect a response on your provided callback. Callbacks could also be fired multiple times (like an event handler) so it's important to keep the connection open.

Callbacks in Prim+RPC are typically useful for real-time information where results are not immediately available but you need a response as soon as one is available, without polling a server at a set interval. This is the case for event handlers where you may expect a response at some point but not immediately as the result of the function call.

As an example, you may compare the usage of a callback plugin to an open WebSocket connection while a method plugin can be likened to a single HTTP request/response (this is the actual case for the Browser API plugins).

Of course, many applications don't need support for callbacks which is why the callback plugin is optional, as long as callbacks are not being used.

You can configure the callback plugin like so:


import { createPrimClient } from "@doseofted/prim-rpc"
import { createCallbackPlugin } from "@doseofted/prim-rpc-plugins/browser-websocket"
const callbackPlugin = createCallbackPlugin()
createPrimClient({ callbackPlugin })

Each callback plugin may (or may not) include its own options specific to that plugin. You can find a list of available callback plugins on this documentation website or you can follow this guide to create your own.

If a callback plugin is given, it's important to note that each plugin used on the Prim+RPC client must have a compatible "handler" plugin on the Prim+RPC server (just like method plugins/handlers).

.jsonHandler

RequiredDefaultSync Required
falseJSON🔴 Yes

Functions used with Prim+RPC have to be serialized before sending a function call (RPC) to the server. By default, any RPC made with Prim+RPC will use the environment's default JSON handler for serialization and unjs/destr for deserialization (which provides the benefit of protection from prototype pollution while behaving predictably).

You can override the default JSON handler as you'd like. The given handler must follow the same/similar signature of the default JSON handler: namely, it needs to have .parse() and .stringify() methods.


import { createPrimClient } from "@doseofted/prim-rpc"
import superjson from "superjson"
createPrimClient({
// superjson has both a stringify and parse method that can be used by Prim+RPC.
jsonHandler: superjson,
})

This can be useful for parsing additional types. For example, superjson is great for parsing additional JavaScript types. A library like devalue is useful for cyclical references. You could even use yaml if readability is top priority. These are only examples and you can use any JSON handler or create your own.

You may also use the default JSON handler for your environment (foregoing safety provided by the default) by setting the JSON handler explictly, like so:


import { createPrimClient } from "@doseofted/prim-rpc"
createPrimClient({
// NOT RECOMMENDED: using unjs/destr for parsing is recommended (and the default)
jsonHandler: { stringify: JSON.stringify, parse: JSON.parse },
})

At this stage of the project, the JSON handler must serialize data into a string. Binary data support for the JSON handler is planned for the future to support other potential JSON-like handlers like @msgpack/msgpack. Of course, you can still upload files using Prim+RPC because, by default, files are extracted from the RPC and sent separately.

It is important to note that this option must be set to the same option on both the server and client since serialization on the client and deserialization on the server go hand-in-hand.

Some clients may not be able to support a custom JSON handler (for instance, native apps that don't use JavaScript). In these instances, you may consider creating two Prim+RPC servers that use the same module: one that uses the default JSON handler, and the second that uses your custom JSON handler (for clients that can support it).

.handleError

RequiredDefaultSync Required
falsetrue🔴 Yes

By default in JavaScript, the Error instance used in JavaScript does not make useful properties like .message enumerable which means that when you try to stringify an Error with JSON you do not get back these properties. Instead, you get an empty object.

To make for an easier development experience, Prim+RPC will serialize thrown errors to be sent back to the client using serialize-error, when using the default JSON handler. This means that if you throw an error from a function on a Prim+RPC server, the Prim+RPC client will receive that same error message. By default, this option is set to true.

If the default JSON handler is changed, for instance if you use superjson which already serializes errors, then this option is set to false so as not to conflict with possible error handling done in the custom JSON handler.

Importantly, if you explicitly specify the .handleError property then your choice will be respected regardless of what JSON handler you are using. The default conditions of setting handleError only apply when the option is not provided by the developer.

This option must be set to the same value on the server and client.

.handleBlobs

RequiredDefaultSync Required
falsetrue🔴 Yes

The Prim+RPC client can call many server-provided functions including those that expect binary arguments. By default, Prim+RPC sends RPC using JSON: a format that doesn't support binary data. In order to support binary data there are two options: The first is overriding the default JSON handler with some JSON-like handler that does support binary data. Support for these handlers is planned for Prim+RPC but is not available yet. The second however is supported and involves separating binary data from the RPC.

The .handleBlobs option tells Prim+RPC to read the given function arguments for anything Blob-like, including Files (which are Blobs). The binary arguments are a replaced with an identifier in the RPC. The configured .methodPlugin is then responsible for sending both the RPC and binary data to the Prim+RPC server. The Prim+RPC server will have a .methodHandler that will piece back together the RPC and binary data into a function call. This is useful because, while JSON cannot support binary data, your method of transport (like using FormData over HTTP) may be able to transport the binary file(s) and JSON separately.

Since splitting up blobs from RPC is done on the client, this option must also be specified on the server so it knows to piece these two pieces back together.

Today, blob handling is done with the method plugin/handler but in the future there are plans to support binary data in the callback handler as well. There's also planned support for JSON-like handlers that support binary data. Prim+RPC is still in early stages.

.endpoint

RequiredDefaultSync Required
false"/prim"🟢 No

It is very common to use Prim+RPC with a web server so the endpoint option is a URL provided directly on the Prim+RPC client. This option is passed to both the methodHandler and the callbackHandler functions as an argument so that they can make connections.

This option can be overridden for the callbackHandler by providing the wsEndpoint option.

Path

None

Full URL

Conditional URL


import { createPrimClient } from "@doseofted/prim-rpc"
createPrimClient({
// Provide the path if the Prim+RPC client/server are on the same domain
endpoint: "/prim",
})

.wsEndpoint

RequiredDefaultSync Required
false""🟢 No

The callbackHandler may use a different URL from the methodHandler. In these events, wsEndpoint can be provided. This is common when the method handler uses the HTTPS protocol while the callback handler uses the WebSocket protocol.

.clientBatchTime

RequiredDefaultSync Required
false0🟢 No

The Prim+RPC client can batch RPCs made with its method handler. This is useful when using Prim+RPC with an HTTP server and you want to avoid sending hundreds of HTTP requests.

By default, Prim+RPC does not batch requests: the batch time is set to zero. If non-zero then Prim+RPC will wait a short time, in milliseconds, before sending HTTP requests. Of course, if one function call depends on another then that will not be batched (because you need the result of the first to make the second call).

As a recommendation, keep this time very low (under 15ms). The default is 0 (don't batch).

This does not apply to the callback handler which will send RPCs one-by-one regardless of this setting. The reason for this that the method handler is used to send a single request and receive a single response, usually over HTTP, which results in additional overhead. The callback handler however will typically keep an open connection so there is less overhead in sending requests, usually over WebSocket, where batching requests isn't as useful.

Server

The following options are available to the createPrimServer:


import { createPrimServer } from "@doseofted/prim-rpc"
import { createMethodHandler, createCallbackHandler } from "@doseofted/prim-rpc-plugins/{CHOOSE_YOUR_HANDLER}"
// Tap option name below to see documentation
// You do not need to provide all of these options. This is just a demonstration.
const server = createPrimServer({
// NOTE: all options from client are available here
module: { sayHello: () => "Hello!" },
methodHandler: createMethodHandler(),
callbackHandler: createCallbackHandler(),
prefix: "/prim",
allowList: { sayHello: true },
methodsOnMethods: [],
showErrorStack: false,
})

.module

RequiredDefaultSync Required
falseundefined🟢 No

When an RPC is received by the Prim+RPC server, it will be translated into a function call to be made on the provided JavaScript module. If a module is not provided then Prim+RPC will return an error.

However, if a module is not provided but a method plugin or callback plugin are given, then the Prim+RPC server will forward that request using the provided plugins (potentially to a separate Prim+RPC instance that does have the module).

Local Module

Inline Module

External Module

Conditional Module


import { createPrimServer } from "@doseofted/prim-rpc"
import * as myLocalModule from "./my-local-module"
createPrimServer({
module: myLocalModule,
})

All functions need to be explicitly allowed before being used with Prim+RPC. This is typically done by adding a .rpc property to each function. When using an external module you may not be able to add a property to an existing function. In these cases you can use the allowList option of Prim+RPC to specify that a module is allowed to be used.

.methodHandler

RequiredDefaultSync Required
false() => void🟢 No

When a function call is made with the Prim+RPC client, that call is serialized into an RPC and is sent using the configured .methodPlugin to the server of your choice. The Prim+RPC server receives these requests from your chosen server with its .methodHandler. In Prim+RPC, each client plugin needs a corresponding server handler.

The method handler in Prim+RPC is a plugin that integrates with your server (for example, an HTTP server) to receive a request containing RPC. The responsibility of the method handler is to transform given server properties into RPC, make the function call, and send back the result to your server in a format that it can understand. Your server then sends a result back to the client.

There are many method handlers available that you integrate with the server framework of your choice. You can also create your own if a server handler doesn't meet your needs or you need support for an additional server. An example of how to use an existing method handler is demonstrated below:


import { createPrimServer } from "@doseofted/prim-rpc"
import { createMethodHandler } from "@doseofted/prim-rpc-plugins/fastify"
// initialize your server (this example uses Fastify)
import Fastify from "fastify"
const fastify = Fastify()
// and pass this instance to the method handler so the server can be configured to accept RPC
createPrimServer({
methodHandler: createMethodHandler({ fastify }),
})
const address = await fastify.listen({ port: 1234 })
console.log("Listening:", address)

Similar to the .methodPlugin, the limitation of method handlers is that they receive a single request (containing one or multiple RPCs) and send a single response (containing one or multiple RPCs). This means that the method handler cannot support callbacks because multiple responses must be sent back (typically by keeping an open connection). Prim+RPC has a dedicated handler for callbacks through the use of a .callbackHandler.

.callbackHandler

RequiredDefaultSync Required
false() => void🟢 No

The callback handler serves a similar role to the .methodHandler and the goal of this handler is very similar. The primary difference is that the callback handler is used to maintain a connection to the client from the server so that callbacks can be called, potentially multiple times.

When a function call is made to Prim+RPC that contains a callback, that function call is turned into an RPC and any callbacks on that function are transformed into a string identifier with the client's .callbackPlugin. This RPC is sent to the server of your choice which then forwards that RPC to the Prim+RPC server's corresponding .callbackHandler.

The callback handler usually is usually a plugin for some kind of event handler where a persistent connection is kept open, like a WebSocket connection. The callback handler's responsibility is to transform RPC from the server into a return value, just like the method handler. However it has an additional responsibility of creating functions in place of the callbacks that you give and then listening for events on them. When a callback is called, those arguments are captured and forwarded to the Prim+RPC client.

There are callback handlers available for the server of your choice and it is also easy to create your own. An example of how to utilize an existing callback handler is given below:


import { createPrimServer } from "@doseofted/prim-rpc"
import { createCallbackHandler } from "@doseofted/prim-rpc-plugins/ws"
const wss = new WebSocketServer({ port: 1234 })
createPrimServer({
callbackHandler: createCallbackHandler({ wss }),
})

.prefix

RequiredDefaultSync Required
false"/prim"🟢 No

This option is used to configure HTTP routers used inside of a method/callback handler. The prefix will become the path for which the Prim+RPC client's plugins will communicate.

In general, if you are using an HTTP/WS server with Prim+RPC then the .prefix option given on the Prim+RPC server is the pathname that you should give as the `.endpoint in the Prim+RPC client. If you are not using a transport that makes use of a URL (i.e. Web Workers, other forms of IPC) then this option isn't needed.

As an example, using some HTTP server:

server.ts

import { createPrimServer } from "@doseofted/prim-rpc"
createPrimServer({
prefix: "/my-rpc-endpoint",
})
// ... start HTTP server

client.ts

import { createPrimClient } from "@doseofted/prim-rpc"
// We'll pretend that the server is running on http://localhost:1234
createPrimClient({
endpoint: "http://localhost:1234/my-rpc-endpoint",
})

This option also tells Prim+RPC what parts of a path to exclude if a request is given over a URL. While most requests are given using the Prim+RPC client, you could also make a simple request like so:


curl --request GET \
--url http://localhost:1234/my-rpc-endpoint/sayHello?name=Ted&greeting=Hello

This example is the equivalent of calling sayHello({ name: "Ted", greeting: "Hello" }). Prim+RPC will know to remove the /my-rpc-endpoint part of the path because we configured it as the .prefix. Making requests over a URL isn't the primary use of Prim+RPC but it is possible as long as the method handler supports it.

.allowList

RequiredDefaultSync Required
false{}🟢 No

All functions provided to Prim+RPC must explicitly be marked usable as RPC. This is typically done by adding a .rpc property to the function in question. Sometimes this is not possible, such as when using a separate package directly with Prim+RPC that does not have an RPC property. In these events, you may specify an allowed list of functions that can be used.

When Prim+RPC receives an RPC it will check the .rpc property and, if not found, follow the allow-list. The allow list is expected to follow the same structure as the module you provide. The allow list below is a simple example and follows the same structure of the module provided:


import { createPrimServer } from "@doseofted/prim-rpc"
// it's easier to specify the `.rpc` property on functions but below is also completely valid
createPrimServer({
module: {
hi() {
return "Hi!"
},
good: {
bye() {
return "Goodbye!"
},
},
},
allowList: {
hi: true,
good: {
bye: true,
},
},
})

.methodsOnMethods

RequiredDefaultSync Required
false[]🟢 No

By default, Prim+RPC will not answer methods calls made on a function. For instance, if you have a function named hello(), you cannot call hello.toString(). However, there may be circumstances where you'd like to allow certain methods on a defined function. For instance, the following could be useful:


function sayHello() {
return "Hello!"
}
sayHello.rpc = true
sayHello.docs = function () {
return "I say hello!"
}

By default, sayHello.docs() is not allowed to be called. This can be fixed by specifying the following:


import { createPrimServer } from "@doseofted/prim-rpc"
import { sayHello } from "./previous-example"
createPrimServer({
module: { sayHello },
methodsOnMethods: ["docs"],
})

Now, any function that has a .docs() method can be called.

It is recommended, but not required, that you choose unique method names and avoid exposing or using the same names as built-in methods, when using this option.

.showErrorStack

RequiredDefaultSync Required
falsefalse🟢 No

When using the .handleError option of Prim+RPC, errors thrown on the server are recreated on the client. This error may possibly include the call stack which generally shouldn't be sent to the client. By default, this property is removed from the error before sending an RPC response.

In development, this stack trace can be useful for tracing an error so you may enable showing this stack when available by toggling this option to true. Care should be taken with this option to ensure that it is not enabled in a production environment.

Testing

Included in Prim+RPC are utilities for running local tests with Prim+RPC, intended to be used with tools like Vitest or Jest.

Documentation in Progress

Prim+RPC: a project by Ted Klingenberg

Dose of Ted

Anonymous analytics collected with Ackee