The Docs.

Prim+RPC is in prerelease mode and may be unstable until official release.

Configuration

Both the Prim+RPC server and client take an options argument to be configured. The server configuration extends the client configuration for two reasons: one, some options should be synced between server and client (such as overrides to RPC serialization) and two, the server can optionally make use of its own client if a function is unavailable.

If an option should be synced to use the same value on both server and client, it will be marked accordingly. You can reference the functions exported by Prim+RPC in the API reference.

Module

  • Sync Required:
    No
function hello() {
	return "Hello from Prim+RPC!"
}
hello.rpc = true
 
createPrimServer({
	module: { hello },
})

The module provided to Prim+RPC’s server contains functions intended to be exposed as RPC on the client. This is just a regular JavaScript module and may either be imported from elsewhere or defined directly on this option.

This option may also be conditionally passed to the client. This is useful in fullstack frameworks where the client has direct access when used on the server and remote access when used off of the server.

In addition to passing your functions to this option, Prim+RPC requires that you explicitly mark functions as RPC, in one of two ways:

  • Define a .rpc property on each of your functions with a value of true
  • Or add your function to the Allow List option

If your function may be accessed by visiting a URL, you may set the .rpc option to "idempotent". If an HTTP method handler has been configured, this will allow the function to be accessed by a GET request (by default, only POST requests are allowed with a method handler).

HTTP Endpoint

  • Sync Required:
    No
const endpoint = "https://example.com/prim"
createPrimClient({ endpoint })

While Prim+RPC can be used with many transports, it’s often used with HTTP servers so, for convenience, an option to pass the server address is given on Prim+RPC itself and is shared with plugins.

WebSocket Endpoint

  • Sync Required:
    No
const wsEndpoint = "wss://example.com/prim"
createPrimClient({ wsEndpoint })

The WebSocket Endpoint option is given for convenience when using a plugin (usually a Callback Handler) that uses WebSockets. If not given, plugins will fallback to using the HTTP Endpoint option (and change the protocol if needed).

Client Batch Time

  • Sync Required:
    No
const clientBatchTime = 15
createPrimClient({ clientBatchTime })

Prim+RPC can batch RPC so that multiple function calls are sent in a single request. By default, Prim+RPC will not batch RPC: the batch time is set to 0. If batching is needed, set this option to a value greater than 0. The value given is the time in milliseconds that Prim+RPC will wait before sending batched requests.

JSON Handler

  • Sync Required:
    Yes
const jsonHandler = { stringify: JSON.stringify, parse: JSON.parse }
createPrimServer({ jsonHandler })

Prim+RPC uses the default JSON “stringify” method to serialize RPC and the destr library to deserialize RPC by default. But this can be overridden by providing your own handler. Despite the name, this doesn’t even necessarily need to be JSON.

JSON handlers must provide two methods:

  • .stringify(rpc): Takes RPC object and serializes into string or binary data
  • .parse(obj): Takes string or binary data and deserializes it into the expected data structure

If your JSON handler serializes into a string containing some form of JSON then this is all you need.

If your JSON handler is another format (like YAML) or a binary format (like MsgPack), you may provide options to help plugins better understand these formats:

  • .binary: a boolean flag that states whether the format is binary or a string
  • .mediaType: the MIME type of the serialized data, useful for transporting over a network.

Examples with popular libraries are given in this documentation.

Method Plugin

  • Sync Required:
    No
import { createMethodPlugin } from "@doseofted/prim-rpc-plugins/browser-fetch"
createPrimClient({ methodPlugin: createMethodPlugin() })

The method plugin takes RPC made from the Prim+RPC client and transforms it into some form of a request that is sent off to another environment. The method plugin is intended to send this data to a corresponding method handler on the server using the same transport. It makes a request to the server and awaits a response for each request.

There are plugins already available to use but you may also create your own.

Callback Plugin

  • Sync Required:
    No
import { createCallbackPlugin } from "@doseofted/prim-rpc-plugins/browser-websocket"
createPrimClient({ callbackPlugin: createCallbackPlugin() })

The client plugin is similar in a lot of ways to the method plugin but works differently under the hood. While the method plugin sends a request to the server and awaits a response, the callback plugin sends a request and then may receive multiple responses. This is because the callback plugin is used to listen to callbacks on the server and those callbacks may fire multiple times.

There are plugins already available to use but you may also create your own.

Handle Error

  • Sync Required:
    Yes
function oops() {
	throw new Error("I did it again")
}
oops.rpc = true
 
createPrimServer({
	module: { hello },
	handleError: true,
})

By default, Prim+RPC handles thrown errors so that errors thrown on the server are also thrown to the client and error mapping can be avoided (for instance, of HTTP codes or error codes). This option is true unless the JSON Handler is overridden in which case it will default to false.

If you explicitly set this option, this will override any default. However, if your JSON handler supports serializing errors and this option is set to true, this may lead to unexpected behavior.

Handle Blobs

  • Sync Required:
    Yes
function index() {
	return new File(["<p>I'm HTML</p>"], "index.html", { type: "text/html" })
}
index.rpc = true
 
createPrimServer({
	prefix: "/",
	module: { default: index },
	handleBlobs: true,
})

When using the default JSON Handler or any JSON handler that serializes to a string, Prim+RPC will separate binary data given from the client or returned from the server into a separate object and replace that binary data with a reference to that object. This is because JSON cannot support binary data but generally Prim+RPC plugins can still transport this information separately.

If your JSON handler supports binary data or you do not want to support binary data used with Prim+RPC, you can toggle this option off.

Allow List

  • Sync Required:
    No
const module = {
	default: () => "Hi!",
	good: { morning: () => "Good morning!" },
}
const allowList = {
	default: true,
	good: { morning: true },
}
createPrimServer({ module, allowList })

By default, functions given to Prim+RPC are not allowed to be called unless a .rpc property is set on the function or that function is added to an allow list.

The Allow List is an object that resembles the module that you give to Prim+RPC where each function name is a key and the value is a boolean flag stating whether that function should be considered RPC.

Methods on Methods

  • Sync Required:
    No
function hello() {
	return "Hello from Prim+RPC!"
}
hello.rpc = true
hello.documentation = () => "I say hello"
 
createPrimServer({
	module: { hello },
	methodsOnMethods: { documentation: true },
})

By default, functions are not allowed to be called with Prim+RPC if, anywhere in the chain, it is defined on another function. The exception to this rule is if a method is defined directly on another method and the name of that method is given in this option. This may be useful for adding metadata to your functions.

Show Error Stack

  • Sync Required:
    No
createPrimServer({
	handleError: true,
	showErrorStack: process.env.NODE_ENV === "development",
})

When the Handle Error option is set to true, Prim+RPC will send the error message to the client. On the server this could potentially include a stack trace. By default, Prim+RPC will not send the stack trace to the client. This option can be set to true to override this behavior. This is useful in development but depending on your security needs may not be wanted in production.

Prefix

  • Sync Required:
    No
const prefix = "/prim"
createPrimServer({ prefix })

The Prefix option is a convenience option to be used by plugins that communicate over a specific address, specifically HTTP and WebSocket servers. It can be used by plugins to establish a path of a URL. By default, it is set to /prim so as not to collide with other possible routes on an existing server. If Prim+RPC is the only service used with a server, you may change this to / (or any other path that you’d like).

Method Handler

  • Sync Required:
    No
import { createMethodHandler } from "@doseofted/prim-rpc-plugins/hono"
import { Hono } from "hono"
 
const app = new Hono()
const methodHandler = createMethodHandler({ app })
createPrimServer({ methodHandler })

The Method Handler takes a request from the server and extracts RPC from it before sending it to the Prim+RPC server. It also formats the RPC back into a response for your chosen server. Each method handler on the server has a corresponding Method Plugin on the client that can understand communicate with it.

There are handlers already available to use but you may also create your own.

Callback Handler

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

The Callback Handler is similar to the Method Handler but is primarily intended to handle callbacks given on functions. The primary difference between the two is that the Method Handler sends a response for every request while the Callback Handler may send multiple responses for a request (because callbacks may be fired multiple times).

It can be used alone or alongside a Method Handler, Prim+RPC is able to determine which plugin to use. Each Callback Handler on the server has a corresponding Callback Plugin on the client.

There are handlers already available to use but you may also create your own.

Pre-Request Hook

  • Sync Required:
    No
import TTLCache from "@isaacs/ttlcache"
 
const cache = new TTLCache({ ttl: 1000 * 5 })
const prim = createPrimClient({
	preRequest(args, name) {
		if (cache.has(name)) return { result: cache.get(name) }
		return { args }
	},
})

The pre-request hook is optional and is executed before sending an RPC to the server. It is given the arguments passed to your function. This may be useful for logging, client-side checks, or caching.

Pre-Call Hook

  • Sync Required:
    No
import { parse } from "valibot"
 
const prim = createPrimServer({
	preCall(args, func) {
		if ("params" in func) return { args: parse(func.params, args) }
		throw new Error("Argument validation is required")
	},
})

The pre-call hook is optional and is executed before calling a given function. It is given the arguments passed to the server and the function that is being called. Do not call the function inside of the hook as Prim+RPC will call this function itself (resulting in the function being called twice). This may be useful for logging, validation, or adding and processing metadata attached to a function.

Post-Call Hook

  • Sync Required:
    No
import { parse } from "valibot"
 
const prim = createPrimServer({
	postCall(returns, func) {
		if ("returns" in func) return parse(func.returns, returns)
		throw new Error("Result validation is required")
	},
})

The post-call hook is optional and is executed after calling a given function. It is given the return value of the function given to the server and the function that was called. Do not call the function inside of the hook as it will be executed a second time.

Post-Request Hook

  • Sync Required:
    No
import TTLCache from "@isaacs/ttlcache"
 
const cache = new TTLCache({ ttl: 1000 * 5 })
const prim = createPrimClient({
	postRequest(result, name) {
		if (!cache.has(name)) cache.set(name, result)
		return result
	},
})

The post-request hook is optional and is executed after receiving an RPC result from the server. It is given the result of your function call. This may be useful for logging, client-side checks, or caching.

Flags (Experimental)

  • Sync Required:
    Yes
const flags = {
	/**
	 * Support return values in functions with multiple promises. This currently
	 * works with the callback handler only. This means only functions with a
	 * callback as an argument can return multiple promises. Once limitation is
	 * removed, this feature is expected be enabled by default.
	 */
	supportMultiplePromiseResults: false,
}
createPrimClient({ flags })
createPrimServer({ flags })

Prim+RPC places some features behind experimental flags. These generally work as intended but may have limitations that will be addressed before enabling by default. Flags should be assumed to be synced between the server and client unless otherwise specified.

Report an Issue