Panrpc
Language-, transport- and serialization-agnostic RPC framework with remote closure support that allows exposing and calling functions on both clients and servers.
Install / Use
/learn @pojntfx/PanrpcREADME
panrpc
Language-, transport- and serialization-agnostic RPC framework with remote closure support that allows exposing and calling functions on both clients and servers.
Overview
panrpc is a flexible high-performance RPC framework designed to work in almost any environment with advanced features such as remote closures and bidirectional RPC calls.
It enables you to:
- Transparently call and expose RPCs in many languages: Thanks to it's use of reflection, panrpc doesn't require you to learn a DSL or run a code generator. RPCs are defined and called as local functions, and its simple protocol means that multiple languages are supported and adding support for new ones is simple.
- Work with any transport layer: Instead of being restricted to one transport layer (like TCP or WebSockets for most RPC frameworks), panrpc depends only on the semantics of a stream or a message, meaning it works over everything from TCP, WebSockets, UNIX sockets, WebRTC, Valkey/Redis, NATS and more.
- Work with any serializer: Instead of being restricted to one serialization framework (like Protobuf or JSON for most RPC frameworks), panrpc can use any user-defined serializer that supports streaming encode/decode, such as JSON, CBOR and others.
- Call RPCs on both clients and servers: Unlike most RPC frameworks, which only allow you to call a server's RPCs from a client, panrpc can also work with the reverse configuration (where the server calls RPCs exposed by the client) or both at the same time.
- Pass closures to RPCs: You can transparently pass closures and callbacks to RPCs as function parameters, and they will be called by the RPC just like if it were a local function call.
Installation
Library
You can add panrpc to your <img alt="Go" src="https://cdn.simpleicons.org/go" style="vertical-align: middle;" height="20" width="20" /> Go project by running the following:
$ go get github.com/pojntfx/panrpc/go/...@latest
For <img alt="typescript" src="https://cdn.simpleicons.org/typescript" style="vertical-align: middle;" height="20" width="20" /> TypeScript, you can add panrpc to your project (both server-side TypeScript/Node.js and all major browser engines are supported) by running the following:
$ npm install @pojntfx/panrpc
purl Tool
In addition to the library, the CLI tool purl is also available; purl is like cURL and gRPCurl, but for panrpc: A command-line tool for interacting with panrpc servers. purl is provided in the form of static binaries.
On Linux, you can install them like so:
$ curl -L -o /tmp/purl "https://github.com/pojntfx/panrpc/releases/latest/download/purl.linux-$(uname -m)"
$ sudo install /tmp/purl /usr/local/bin
On macOS, you can use the following:
$ curl -L -o /tmp/purl "https://github.com/pojntfx/panrpc/releases/latest/download/purl.darwin-$(uname -m)"
$ sudo install /tmp/purl /usr/local/bin
On Windows, the following should work (using PowerShell as administrator):
Invoke-WebRequest https://github.com/pojntfx/panrpc/releases/latest/download/purl.windows-x86_64.exe -OutFile \Windows\System32\purl.exe
You can find binaries for more operating systems and architectures on GitHub releases.
Tutorial
<img alt="Go" src="https://cdn.simpleicons.org/go" style="vertical-align: middle;" height="20" width="20" /> Go
Just looking for sample code? Check out the sources for the example coffee machine server and coffee machine client/remote control.
1. Choosing a Transport and Serializer
<details> <summary>Expand section</summary>Start by creating a new Go module for the tutorial and installing github.com/pojntfx/panrpc/go:
$ mkdir -p panrpc-tutorial-go
$ cd panrpc-tutorial-go
$ go mod init panrpc-tutorial-go
$ go get github.com/pojntfx/panrpc/go/...@latest
The Go version of panrpc supports many transports. While common ones are TCP, WebSockets, UNIX sockets or WebRTC, anything that directly implements or can be adapted to a io.ReadWriter can be used with the panrpc LinkStream API. If you want to use a message broker like Valkey/Redis or NATS as the transport, or need more control over the wire protocol, you can use the LinkMessage API instead. For this tutorial, we'll be using WebSockets as the transport through the github.com/coder/websocket library, which you can install like so:
$ go get github.com/coder/websocket@latest
In addition to supporting many transports, the Go version of panrpc also supports different serializers. Common ones are JSON and CBOR, but similarly to transports anything that implements or can be adapted to a io.ReadWriter stream can be used. For this tutorial, we'll be using JSON as the serializer through the encoding/json Go standard library.
2. Creating a Server
In this tutorial we'll be creating a simple coffee machine server that simulates brewing coffee, and can be controlled by using a remote control (the coffee machine client).
<details> <summary>Expand section</summary>To start with implementing the coffee machine server, create a new file cmd/coffee-machine/main.go and define a basic struct with a BrewCoffee method. This method simulates brewing coffee by validating the coffee variant, checking if there is enough water available to brew the coffee, sleeping for five seconds, and returning the new water level to the remote control:
// cmd/coffee-machine/main.go
package main
import (
"context"
"errors"
"log"
"slices"
"time"
)
type coffeeMachine struct {
supportedVariants []string
waterLevel int
}
func (s *coffeeMachine) BrewCoffee(
ctx context.Context,
variant string,
size int,
) (int, error) {
if !slices.Contains(s.supportedVariants, variant) {
return 0, errors.New("unsupported variant")
}
if s.waterLevel-size < 0 {
return 0, errors.New("not enough water")
}
log.Println("Brewing coffee variant", variant, "in size", size, "ml")
time.Sleep(time.Second * 5)
s.waterLevel -= size
return s.waterLevel, nil
}
The following limitations on which methods can be exposed as RPCs exist:
- Methods must have
context.Contextas their first argument- Methods can not have variadic arguments
- Methods must return either an error or a single value and an error
- Methods must be public (private methods won't be callable as RPCs, but stay callable as regular methods)
To start turning the BrewCoffee method into an RPC, create an instance of the struct and pass it to a panrpc Registry like so:
// cmd/coffee-machine/main.go
import "github.com/pojntfx/panrpc/go/pkg/rpc"
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
service := &coffeeMachine{
supportedVariants: []string{"latte", "americano"},
waterLevel: 1000,
}
var clients atomic.Int64
registry := rpc.NewRegistry[struct{}, json.RawMessage](
service,
&rpc.RegistryHooks{
OnClientConnect: func(remoteID string) {
log.Printf("%v remote controls connected", clients.Add(1))
},
OnClientDisconnect: func(remoteID string) {
log.Printf("%v remote controls connected", clients.Add(-1))
},
},
)
}
Now that we have a registry that provides our coffee machine's RPCs, we can link it to our transport (WebSockets) and serializer of choice (JSON). This requires a bit of boilerplate to upgrade from HTTP to WebSockets, so feel free to copy-and-paste this, or take a look at the examples to check out how you can set up a different transport (TCP, WebSockets, UNIX sockets etc.) and serializer (JSON, CBOR etc.) instead:
<details> <summary>Expand boilerplate code snippet</summary>// cmd/coffee-machine/main.go
import (
"encoding/json"
"net"
"net/http"
"github.com/pojntfx/panrpc/go/pkg/rpc"
"github.com/coder/websocket"
)
func main() {
// ...
// Create TCP listener
lis, err := net.Listen("tcp", "127.0.0.1:1337")
if err != nil {
panic(err)
}
defer lis.Close()
log.Println("Listening on", lis.Addr())
// Create HTTP server from TCP listener
if err := http.Serve(lis, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
w.WriteHeader(http.StatusInternalServerError)
log.Printf("Remote contro
