The Docs.

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

Create a Plugin

Prim+RPC does not include methods to transport RPC by default. Instead, it relies on plugins so that the core of the library is framework-agnostic. It could be used with HTTP servers, WebSocket servers, between Web Workers, separate processes, or other transports.

This is an advanced guide and is not required for most usages of Prim+RPC.

Prim+RPC communicates between the server and client over two types of plugins, each with a version for the server and client. Method plugins handle calls to methods. Callback plugins optionally handle callbacks on those methods. Plugins used on the server are referred to as handlers to easily differentiate server/client.

In this guide we will learn how to create our own plugins for Prim+RPC. Note that there are many plugins already available to use and you may not need to create your own. This may be considered an advanced topic for most users and is not required for most usages of Prim+RPC.

Method Plugin

A Method Plugin is given RPC as an object and returns back a promise containing an RPC result object given from a Method Handler. Every method plugin may be started from the following skeleton:

import { type PrimClientMethodPlugin, createPrimClient } from "@doseofted/prim-rpc"
 
interface Options {
	/* (options for your plugin) */
}
export function createMethodPlugin(options?: Options): PrimClientMethodPlugin {
	return async function (endpoint, rpcBody, jsonHandler, blobs) {
		/* (plugin code here) */
	}
}
 
/* (this method plugin can be passed to Prim+RPC) */
const methodPlugin = createMethodPlugin()
const client = createPrimClient({ methodPlugin })

The createMethodPlugin function is configured with your options and returns a function compatible with Prim+RPC’s method plugin option. In this function, your plugin is given the following arguments:

  • endpoint is the URL given to Prim+RPC. This is useful if your plugin communicates over a URL, otherwise it may be ignored.
  • rpcBody is the RPC to be sent, as an object. If batching is enabled, this will be an array of RPC calls. It should be processed by the given JSON Handler.
  • jsonHandler is the JSON Handler configured with Prim+RPC. This option should be used instead of the environment’s default JSON object.
  • blobs is a key/value record of binary blobs. This is only used if Prim+RPC is configured to handle Blobs. The key is a string identifier used in the RPC (in rpcBody) and the value is a File or Blob.

The return value of this function should be the RPC result as an object. Your transport (method handler) may returned a serialized version of this RPC. It should be deserialized using the provided jsonHandler argument.

You may reference existing plugin code to learn how they typically transport RPC over protocols like HTTP, WebSocket, or others.

Method Handler

A Method Handler is given RPC from the transport of your choice, provided by a Method Plugin, and sends this data back to your transport, to be handled again by the Method Plugin. The Prim+RPC “server” is a misnomer and doesn’t actually serve RPC, instead this function is a wrapper around setup performed by the server of your choice.

sequenceDiagram
Prim RPC Client->>Method Plugin: RPC call(s)
Method Plugin->>Server: Request with RPC(s)
Server->>Method Handler: Generic request with RPC(s)
Method Handler->>Prim RPC Server: RPC call(s)
Prim RPC Server->>Method Handler: RPC result(s)
Method Handler->>Server: Generic response with RPC result(s)
Server->>Method Plugin: Response with RPC result(s)
Method Plugin->>Prim RPC Client: RPC result(s)

Server Handlers are used with Prim+RPC in one of two ways, depending on how the transport is configured. You may either pass a Method Handler function to Prim+RPC or you may pass a Prim+RPC instance to your Method Handler. The latter is only used when your server exports functions which are called by your server (this is the case in many serverless environments).

Below is a basic skeleton of a method handler:

import { type PrimServerMethodHandler, createPrimServer } from "@doseofted/prim-rpc"
 
interface Options {
	/* (options for your handler plugin) */
}
export function createMethodHandler(options: Options): PrimServerMethodHandler {
	return prim => {
		/* (handler registration here) */
		const {} = prim.server()
	}
}
 
const methodHandler = createMethodHandler()
const prim = createPrimServer({ methodHandler })

The createMethodHandler function is configured with your options and returns a function compatible with Prim+RPC’s Method Handler option. In this function, your handler is given a single argument prim which contains the following:

  • prim.options: Options as given to the Prim+RPC instance
  • prim.server(): a function that creates a new Prim+RPC server instance

Your method handler is only called once on Prim+RPC server initialization. Inside of this function, you should set up the server of your choice to respond to requests that come in. For instance if setting up an HTTP server, this would be a configured route handler (and your HTTP server would then become an option of your setup function).

For each request that comes into your server, call the prim.server() function. This function should be called for every request that comes into your server framework. That may look something like this (this example assumes some connect-based server is used):

import { type PrimServerMethodHandler, createPrimServer } from "@doseofted/prim-rpc"
 
interface Options {
	/* (options for your handler plugin) */
	app: any
}
export function createMethodHandler(options: Options): PrimServerMethodHandler {
	const { app } = options
	return prim => {
		/* (example handler registration here) */
		app.use((req, res, next) => {
			const { prepareCall, prepareRpc, prepareSend, call } = prim.server()
			// ...
		})
	}
}
 
const methodHandler = createMethodHandler()
const prim = createPrimServer({ methodHandler })

The result of this function call to prim.server() contains several methods to transform requests into RPC that Prim+RPC can understand.

  • server.prepareCall() takes common server options and transforms it into an RPC object.
  • server.prepareRpc() takes result of server.prepareCall() and creates an RPC result object from the function call. It also takes an optional HTTP method argument if used over a network (if not used on a network, set this argument to false).
  • server.prepareSend() takes the result of server.prepareRpc() and serializes it into common server response options.
  • server.call() is a shortcut that calls all of the above methods in order. This is useful in HTTP/WebSocket server environments but no so much in others (such as IPC).

When setting up a method handler to be used with Prim+RPC, you may use all of these options. If your method handler does not use HTTP, you may only need to use server.prepareRpc(). If only server.prepareRpc() you may need to serialize RPC using the JSON Handler given in prim.options.

The result of calling these functions should be passed back to your configured server framework to send back a response. You may learn how to implement your own by referencing existing plugin code.

Callback Plugin

A Callback Plugin is given options from the Prim+RPC client and returns back functions to be called whenever a new function call is made from the client. Your chosen transport will receive new messages (from the Callback Handler) and convert this into an RPC result that is then shared with the Prim+RPC client.

The skeleton of a Callback Plugin may look like this:

import { type PrimClientCallbackPlugin, createPrimClient } from "@doseofted/prim-rpc"
 
interface Options {
	/* (options for your plugin) */
}
export function createCallbackPlugin(options: Options): PrimClientCallbackPlugin {
	return (endpoint, events, jsonHandler) => {
		/* (plugin implementation here) */
		return {
			send(rpcBody) {},
			close() {},
		}
	}
}
 
/* (this callback plugin can be passed to Prim+RPC) */
const callbackPlugin = createCallbackPlugin()
const client = createPrimClient({ callbackPlugin })

The createCallbackPlugin function is configured with your options and returns a function compatible with Prim+RPC’s Callback Plugin option. In this function, your plugin is given the following arguments:

  • endpoint is the URL given to Prim+RPC. This is useful if your plugin communicates over a URL, otherwise it may be ignored.
  • events is an object that contains three functions: events.connected() when the transport connects, events.response() when a new message is received, and events.ended() when the transport disconnects.
  • jsonHandler is the JSON Handler configured with Prim+RPC. This option should be used instead of the environment’s default JSON object inside of your defined send() method and the events.response() function.

The Callback Plugin is only called once for each new connection over your transport. The existing plugin will continue to be used until you call events.ended() which will signal that this plugin can no longer be used without re-initializing. Somewhere in your transport you should listen for new messages from the server and send these back using events.response() with the RPC result as an argument. You may serialize that argument using the provided JSON Handler.

With an active connection, the functions you return from your Callback Plugin will be called on all subsequent events. The send() method will send a new message over your transport and the close() method will terminate an active connection.

You may reference existing plugin code to learn how they typically transport RPC over protocols like HTTP, WebSocket, or others.

Callback Handler

A Callback Handler is given new messages from the transport of your choice, provided by a Callback Plugin, and sends this data back to your transport. In addition, it maintains that connection to send additional messages for the same request over your transport. This is useful for callbacks which may fire multiple times.

sequenceDiagram
Prim RPC Client->>Callback Plugin: Upgraded needed
Callback Plugin->>Server: Make connection
Server->>Callback Handler: Make generic connection
Callback Handler->>Prim RPC Server: Connect
Server->>Callback Plugin: Connected
Callback Plugin->>Prim RPC Client: Ready
Prim RPC Client->>Callback Plugin: RPC call
Callback Plugin->>Server: Request with RPC
Server->>Callback Handler: Generic request with RPC
Callback Handler->>Prim RPC Server: RPC call
Prim RPC Server->>Callback Handler: RPC result
Callback Handler->>Server: Generic response with RPC result
Server->>Callback Plugin: Response with RPC result
Callback Plugin->>Prim RPC Client: RPC result
Prim RPC Server->>Callback Handler: RPC callback results
Callback Handler->>Server: Generic response with RPC callback results
Server->>Callback Plugin: Response with RPC callback results
Callback Plugin->>Prim RPC Client: RPC callback results
Server->>Callback Plugin: Disconnected
Callback Plugin->>Prim RPC Client: Disconnect
Server->>Callback Handler: Disconnected
Callback Handler->>Prim RPC Server: Disconnect

The diagram is somewhat intimidating but the implementation is less complicated, as steps like making the connection and sending callback results is largely handled by other frameworks rather than your plugin: you just need to support the given events.

Below is a skeleton of what a callback handler may look like:

import { type PrimServerCallbackHandler, createPrimServer } from "@doseofted/prim-rpc"
 
interface Options {
	/* (options for your handler plugin) */
}
export function createCallbackHandler(options: Options): PrimServerCallbackHandler {
	return prim => {
		/* (handler registration here) */
	}
}
 
const callbackHandler = createCallbackHandler()
const prim = createPrimServer({ callbackHandler })

The createCallbackHandler function is configured with your options and returns a function compatible with Prim+RPC’s Callback Handler option. In this function, your handler is given a single argument prim which contains the following:

  • prim.options: Options as given to the Prim+RPC instance
  • prim.connected(): a function that creates a new active Prim+RPC server instance.

Unlike the Method Handler’s prim.server() option, the prim.connected() event is intended to keep an active connection to the client as opposed to sending a single response and then closing the connection. This means that servers used with the callback handler must have the ability to maintain a connection (like WebSocket or an HTTP Stream).

Note that the prim.connected() event should be called upon every new connection to your server. Inside of this function, you would configure the server of your choice to respond to requests that come in. For instance, if setting up a WebSocket server, this would listen for new “connected” events.

Below is how a generic WebSocket-like server would be configured to accept new connections:

import { type PrimServerCallbackHandler, createPrimServer } from "@doseofted/prim-rpc"
 
interface Options {
	/* (options for your handler plugin) */
	websocket: any
}
export function createCallbackHandler(options: Options): PrimServerCallbackHandler {
	return prim => {
		/* (example handler registration here) */
		websocket.on("connection", connection => {
			const { ended, call } = prim.connected()
			connection.on("close", ended)
			connection.on("error", ended)
			connection.on("message", message => {
				// ...
			})
		})
	}
}
 
const callbackHandler = createCallbackHandler()
const prim = createPrimServer({ callbackHandler })

The result of this function call to prim.connected() contains several methods to transform requests into RPC that Prim+RPC can understand:

  • connected.call() is a function that should be called when a new message is received from the client. This will transform the data using the configured JSON handler and call the function.
  • connected.rpc() is an alternative function that can be called when you want to serialize messages using the given JSON Handler yourself.
  • connected.ended() is a function that should be called when the connection is closed. This will signal to Prim+RPC that this connection is no longer active.

When using connected.call() or connected.rpc(), ensure that the result of these calls is sent back to your server so that the corresponding Callback Plugin on the client can receive the messages.

The result of calling these functions should be passed back to your configured server framework to send back any possible results created for your function call. You may learn how to implement your own by referencing existing plugin code.

Report an Issue