RPC Docs.
Prim+RPC works over the transport of your choice through the usage of plugins. There are four types of plugins, split into two categories: a client plugin that sends RPC and expects a result, and a server handler that receives given RPC and returns a result.
You may not need to create your own plugin. Check out the available plugins for the server, plugins for the client, and plugins for process communication.
On both the client and the server, there are two types of plugins/handlers. There are method handlers used to send RPC and receive an immediate RPC result and callback handlers which are used to send RPC and await multiple results over time. Each method handler on the server-side works with the client through a method plugin. The same applies to callbacks: each server-side handler must have a client-side plugin.
For example: if you're already using Express as an HTTP server then you may want to use Prim+RPC with the Express method handler on the server. Since you're using an HTTP server, you'll also need a client-side plugin that can send and receive HTTP requests. For this purpose, you may use the Fetch method plugin which uses the web browser's Fetch API to communicate with your server.
As another example, you may want to support callbacks on your function calls used with Prim+RPC. For this, you'll need a callback handler. If you are using "ws" (a WebSocket library for Node) then you may use the ws callback handler. You'll also need a corresponding callback plugin on the client. Since we're using a WebSocket on the server, we'll use the WebSocket callback plugin which uses the web browser's WebSocket API to communicate with your server. You may use the method and callback plugins at the same time with Prim+RPC because it will know when to use one versus the other.
Now that we understand the responsibilities of each plugin/handler, we can determine what type of plugin that we'd like to create and learn how to create it next.
The method plugin is given options from the Prim+RPC client and is called whenever it needs to communicate with the Prim+RPC server. Every method plugin starts with the following skeleton:
import type { PrimClientMethodPlugin } from "@doseofted/prim-rpc"interface Options { /* (options for your plugin) */}export function createMethodPlugin(options?: Options): PrimClientMethodPlugin { return async (endpoint, jsonBody, jsonHandler, blobs) => { // ... plugin code here }}// this method plugin can be passed to Prim+RPCconst methodPlugin = createMethodPlugin()
The createMethodPlugin
function returns a function that is registered with Prim+RPC. Whenever Prim+RPC calls this
function, it will populate the parameters of your function. The endpoint
is a URL configured on the Prim+RPC client's
.endpoint
option (only needed if your plugin communicates over a URL). The jsonBody
parameter is either a single RPC
call or, if batching is enabled, a list of RPC calls. This is given as a regular JavaScript object. Most transports
can't send this object without serialization so the next parameter, jsonHandler
, is a JSON handler that has an
interface resembling JSON
(the JSON handler may be overridden in the client but is expected to follow the same
interface). The last parameter is blobs
and is only used if your JSON handler does not support file uploads (most will
not). This is a key/value record where the key is a string identifier used in the RPC (in jsonBody
) and the value is
the binary file. Since most JSON-like handlers can't support binary objects, it is expected to handle this parameter
separately in your communication channel from the RPC (for example, use FormData to send both the RPC and binary blobs).
You may browse the source code of available method plugins to see full examples but we'll demonstrate a simple example like so:
import type { PrimClientMethodPlugin } from "@doseofted/prim-rpc"interface Options { headers?: Record<string, string>}export function createMethodPlugin(options?: Options): PrimClientMethodPlugin { const { headers = {} } = options ?? {} return async (endpoint, jsonBody, jsonHandler, blobs) => { const result = await fetch(endpoint, { method: "POST", headers: { "content-type": jsonHandler.mediaType, ...headers }, body: jsonHandler.stringify(jsonBody), }) return jsonHandler.parse(await result.text()) }}// this method plugin can be passed to Prim+RPCconst methodPlugin = createMethodPlugin({ headers: { "x-my-example-header": "hello", },})
Of course, this example is intended to be simple. It doesn't have additional handling of errors or support for the blobs parameter. However, this does demonstrate how you can send off RPC requests to a server.
This example shows how to send off an RPC request. In the next example, we'll look at how to create a method handler that can read this request in Prim+RPC.
The method handler plugin is given methods from the Prim+RPC server and is called whenever a request comes in from your configured server. Despite being called the Prim+RPC "server", Prim+RPC does not include a server in itself. Instead, requests are given to the server of your choice (for instance, an HTTP framework like Express) and then that server forwards the request to Prim+RPC. These requests are handled by Prim+RPC with either a method or callback "handler" plugin. Every method handler starts with a skeleton like so:
import type { PrimServerMethodHandler } from "@doseofted/prim-rpc"interface Options { /* (options for your handler plugin) */}export function createMethodHandler(options: Options): PrimServerMethodHandler { return prim => { // ... handler registration here }}
The createMethodHandler
function configures the server with the options you pass to it. Generally, you pass this
function an instance of your server. In the function returned (with prim
parameter), you set up Prim+RPC with your
server framework. In server frameworks like Express this would be considered middleware (in Fastify, a plugin). Below is
an example that configures a generic Connect middleware (popular with frameworks like Express) as a method handler in
Prim+RPC:
import type { PrimServerMethodHandler } from "@doseofted/prim-rpc"import type { Application } from "express"interface Options { app: Application}export function createMethodHandler(options: Options): PrimServerMethodHandler { return prim => { app.use((req, res, next) => { const server = prim.server() if (req.path.startsWith(prim.options.prefix)) { server .call({ url: req.url, body: req.body, method: req.method, }) .then(result => { res.status(result.status).set(result.headers).send(result.body) }) } else { next() } }) }}
The prim
parameter given on the method handler is populated by Prim+RPC and includes methods to call a function based
on RPC given to your server framework. For every request, you call prim.server()
to create a new instance of the
Prim+RPC server. This returns several methods that can be used in a request. For most HTTP server frameworks, you can
use server.call()
to pass common HTTP-specific parameters and get back common HTTP response options. Alternatively,
you may call server.prepareCall()
, server.prepareRpc()
, then server.prepareSend()
successively (this is what
server.call()
is actually doing). If you're not using an HTTP server, then you may want to use just
server.prepareRpc()
to pass RPC directly to Prim+RPC and receive the result back.
Like the method plugin example prior, this is a simplified example and doesn't consider conditions such as usage of other middleware, errors, blobs or otherwise. You may refer to existing built plugins to see how they are built to learn more.
The callback plugin is given options from the Prim+RPC client and is called whenever a function is called on the client that makes use of a callback. In order to respond to multiple calls of the callback, a persistent connection is established. The skeleton of this plugin looks like this:
import type { PrimClientCallbackPlugin } from "@doseofted/prim-rpc"interface Options { /* (options for your plugin) */}export function createCallbackPlugin(options: Options): PrimClientCallbackPlugin { return (endpoint, client, jsonHandler) => { // ... plugin code here return { send(message) {}, close() {}, } }}// this method plugin can be passed to Prim+RPCconst callbackPlugin = createCallbackPlugin()
The createCallbackPlugin
returns a function that is registered with Prim+RPC. This function is given several
parameters including the endpoint (useful when given a URL on Prim+RPC client on .endpoint
or .wsEndpoint
), the
client
options (including several methods which we'll cover soon), and a jsonHandler
for serializing requests and
responses over your chosen transport.
The client
parameter contains methods that expected to be called on server events. When a new connection is formed,
call client.connected()
. When that connection is closed, call client.ended()
. When a response is given from the
Prim+RPC server, call client.response()
.
This function also returns methods that are used by Prim+RPC: .send()
will send messages off to the Prim+RPC server
and .close()
will close the connection to the server.
We can demonstrate how this plugin works with an example:
import type { PrimClientCallbackPlugin } from "@doseofted/prim-rpc"interface Options { /* (options for your plugin) */}export function createCallbackPlugin(options?: Options): PrimClientCallbackPlugin { return (endpoint, client, jsonHandler) => { const ws = new WebSocket(endpoint) ws.addEventListener("open", client.connected) ws.addEventListener("close", client.ended) ws.addEventListener("message", ({ data }) => { response(jsonHandler(data)) }) return { send(data) { ws.send(jsonHandler.stringify(data)) }, close() { ws.close() }, } }}// this method plugin can be passed to Prim+RPCconst callbackPlugin = createCallbackPlugin()
Now we can take a look at the corresponding callback handler for which this plugin communicates.
The callback handler plugin is called whenever a method is called that contains a callback. The server receives method calls from the server of your choice and returns a result. Unlike the method handler, additional responses may be sent back if a callback is called, using an established connection (such as that over a WebSocket, for example). The skeleton of this handler plugin is like so:
import type { PrimServerCallbackHandler } from "@doseofted/prim-rpc"interface Options { /* (options for your handler plugin) */}export function createCallbackHandler(options: Options): PrimServerCallbackHandler { return prim => { // ... handler registration here }}
The createCallbackHandler
configures your chosen server with options you provide to it and returns a function that is
used by Prim+RPC to register the handler plugin. In this function, you should configure the given server framework. The
prim
parameter on this returned function contains a method connected()
that you can call when a new connection is
made to the server. This function, in turn, returns three new methods: rpc()
, call()
, and ended()
. Once you
receive a new request to your server, you can call either call()
if given a string of data (this function has a
callback that gives you a string result) or you can call rpc()
if given an RPC object (this function has a callback
that gives you the RPC result). When the connection is closed, you can use the ended()
method to signal this to
Prim+RPC.
Below is an example of a callback handler that uses WebSocket:
import type { PrimServerCallbackHandler } from "@doseofted/prim-rpc"import type { WebSocketServer } from "ws"interface Options { wss: WebSocketServer}export function createCallbackHandler(options: Options): PrimServerCallbackHandler { const { wss } = options return prim => { webSocketServer.on("connection", (ws, req) => { const { call, ended } = prim.connected() ws.on("close", ended) ws.on("message", data => { call(data, message => { ws.send(message) }) }) }) }}
Like other examples in this guide, this demonstrates the basics of setting up a new plugin with Prim+RPC and could be a starting point but additional situations may need to be considered such as error handling.
You can reference the existing plugins and handlers for Prim+RPC as examples of plugins and handlers.