RPC Docs.

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

Features and How to Use

If you know a little JavaScript then you know how to use Prim+RPC. In this guide, we'll outline the features of Prim+RPC and give examples of how to use them.

It is assumed that you have already set up the Prim+RPC client and server. If you have not yet then you may follow the setup guide before continuing. The following are partial examples that demonstrate each selected feature. We'll be working out of an imaginary file called example.ts (a TypeScript file but feel free to use regular JavaScript). For example's sake, we'll assume that this file is imported and used with Prim+RPC like so:

index.ts
example.ts

import { createPrimClient, createPrimServer, testing } from "@doseofted/prim-rpc"
import * as module from "./example"
const plugins = testing.createPrimTestingPlugins()
const { callbackHandler, methodHandler } = plugins
const server = createPrimServer({ module, callbackHandler, methodHandler })
const { methodPlugin, callbackPlugin } = plugins
const client = createPrimClient({ callbackPlugin, methodPlugin })

Of course, if you'd like to follow along then you can use any plugins that you'd like. The following examples will modify the module given in example.ts.

It's also worth noting that this guide is only meant to demonstrate usage of functions with Prim+RPC. You can find more features available with the Prim+RPC server and client by checking the configuration reference.

Table of Contents

Call a Function

You can create a regular JavaScript function and use that directly with Prim+RPC. Remember to add the .rpc property to the function to tell Prim+RPC that it is allowed to be called. Also remember that the function result is wrapped in a Promise and must be awaited (because we must await a response from the server).

example.ts

/** Say hello! */
export function sayHello(name: string) {
return `Hello ${name ?? "you"}!`
}
sayHello.rpc = true

index.ts

// ...client defined earlier
const greeting = await client.sayHello("Ted")
console.log(greeting) // "Hello Ted!"

By default, Prim+PRC can accept all types supported by JSON. These can be extended by using a custom JSON handler as seen in the next example.

Prim+RPC should be configured with a .methodHandler on the server and a .methodPlugin on the client for method calls to work.

Pass Advanced Arguments

If you've set up a custom JSON handler then you can even use other non-primitive types like Dates directly with Prim+RPC!

example.ts

/** Probably tomorrow. */
export function whatIsDayAfter(day: Date) {
return new Date(day.valueOf() + 1000 * 60 * 60 * 24)
}
whatIsDayAfter.rpc = true

index.ts

// ...client defined earlier
const result = await client.whatIsDayAfter(new Date())
console.log(result) // (a Date object)

You can also pass callbacks and files to functions!

Throw an Error

If a function that you define on the server throws an error, that same error can be thrown on the client (as long as .handleError option is true).

example.ts

/** Whoopsie */
export function oops(ok = false) {
if (!ok) {
throw new Error("My bad.")
}
return "I did it again."
}
oops.rpc = true

index.ts

// ...client defined earlier
try {
const result = await client.oops()
} catch (error) {
if (error instanceof Error) {
console.log(error.message) // "My bad."
}
}

Use a Callback

Functions used with Prim+RPC can accept callbacks. This is useful for listening to events on the server as they happen without having to poll the server at a set interval.

example.ts

/** Type a message. Now with configurable type speed. */
export function typeMessage(message: string, typeLetter: (typed: string) => void, speed = 300) {
let timeout = 0
message.split("").forEach(letter => {
setTimeout(() => typeLetter(letter), ++timeout * speed)
})
}
typeMessage.rpc = true

index.ts

// ...client defined earlier
await client.typeMessage("Hello!", letter => {
console.log(letter) // each letter of "Hello!"
})

Prim+RPC should be configured with a .callbackHandler on the server and a .callbackPlugin on the client for callbacks to work.

Use Multiple Signatures

As you'll see in the next few examples, you can use multiple signatures on a function when using TypeScript. This is useful when accepting file uploads or uploading an HTML form where the type on the client is not the same as or is unavailable on the server.

example.ts

/** Add two numbers together. Strings are converted to numbers automatically. */
export function add(x: string, y: string): number
/** Add two numbers together. */
export function add(x: number, y: number): number
export function add(x: number | string, y: number | string) {
return Number(x) + Number(y)
}
add.rpc = true

index.ts

// ...client defined earlier
const result1 = await client.add(5, 5)
const result2 = await client.add("7", "3")
console.log(result1, result1 === result2) // 10, true

Upload Files

You can upload files to Prim+RPC as long as your configured .methodHandler supports it. Files can be sent like so (this example uses Node):

example.ts

import fs from "node:fs/promises"
import path from "node:path"
/** Upload a file to the server */
export function uploadFile(file: File) {
try {
await fs.writeFile(path.resolve("./uploads"), Buffer.from(await file.arrayBuffer()))
console.log("Saved uploaded file:", file.name)
} catch (error) {
console.error("Failed to saved:", file.name, error)
}
}
uploadFile.rpc = true

index.ts

// ...client defined earlier
const file = new File(["Hello from client!"], "hello.txt", { type: "text/plain" })
await client.uploadFile(file) // server will log file name and save to disk

This function has two function signatures (when using TypeScript). The first signature is for the browser where the client will provide a File. On the server, a Prim+RPC plugin will read the file, save it to a temporary location, and then pass the location of the file back to the function wrapped in a Promise (since uploading the file from the client isn't immediate).

The current file-handling behavior of method handlers may be changed in the future to support Buffers or (possibly) Streams. Prim+RPC is in early stages.

Pass a Form Directly

If you have an HTML form on the page, you can pass that Form element directly to Prim+RPC. The Prim+RPC client will read the FormData given on the form and turn it into an object (preserving any files given on the Form) that will be sent to the server.

example.ts

interface FormInputs {
name: string
email: string
message: string
}
/** TypeScript/server utility to ensure given argument is of type T */
const server = <T,>(given: unknown): given is T => typeof window === "undefined"
/** Submit a contact form */
export function submitContactForm(form: HTMLFormElement | FormData): string
export function submitContactForm(inputs: FormInputs): string
export function submitContactForm(inputs: unknown) {
if (!server<FormInputs>(inputs)) {
return
}
const { name, message, email } = inputs
console.log("Details to be sent somewhere:", { name, message, email })
return `Thank you for your message, ${name}!`
}
submitContactForm.rpc = true

index.ts

// ...client defined earlier
const form = document.getElementById("my-html-form") as HTMLFormElement
const thankYouMessage = await client.submitContactForm(form)
console.log(thankYouMessage) // `Thank you for your message, Ted!`

It's assumed that the HTML form used in this example has three text/email inputs named name, email, and message.

Note that we defined a function signature in TypeScript intended for the browser (accepting a HTMLFormElement) and the function signature for the server is an object with the names of the given inputs (FormInputs).

Use Nested Modules

Functions don't have to be located at the top of your module in Prim+RPC. You can use nested modules like so:

example.ts

/** Say hello! */
function sayHello(name: string) {
return `Hello ${name ?? "you"}!`
}
sayHello.rpc = true
const moduleLike = {
i: {
would: {
like: {
to: {
sayHello
}
}
}
}
}
export moduleLike

index.ts

// ...client defined earlier
const greeting = await client.i.would.like.to.sayHello("Ted")
console.log(greeting) // "Hello Ted!"

Use Server Context

You can set the context of the function based on the utilized method and callback handlers. Each handler used with Prim+RPC will have its own context (for instance, a Request object in some server framework). This context can be accessed from the function's this object (Prim+RPC will bind that context to the function).

You may override this context or limit what is bound to functions by setting the .contextTransform option, if available, on the given handler.

example.ts

/** Perform action on server if logged in */
export function performAdministrativeAction(): string
export function performAdministrativeAction(this: PrimFastifyContext) {
let allowed = false
if (this.cookie.authorized) {
allowed = true
}
return allowed ? "You're in!" : "Oops! Try again."
}
performAdministrativeAction.rpc = true

index.ts

// ...client defined earlier
// we'll pretend that we're already authorized
const message = await client.performAdministrativeAction()
console.log(message) // "You're in!"

Prim+RPC: a project by Ted Klingenberg

Dose of Ted

Anonymous analytics collected with Ackee