{"id":22801731,"url":"https://github.com/pojntfx/panrpc","last_synced_at":"2025-04-19T18:37:59.256Z","repository":{"id":63389319,"uuid":"564495403","full_name":"pojntfx/panrpc","owner":"pojntfx","description":"Language-, transport- and serialization-agnostic RPC framework with remote closure support that allows exposing and calling functions on both clients and servers.","archived":false,"fork":false,"pushed_at":"2025-01-14T16:55:48.000Z","size":11392,"stargazers_count":37,"open_issues_count":0,"forks_count":0,"subscribers_count":4,"default_branch":"main","last_synced_at":"2025-04-14T04:55:25.276Z","etag":null,"topics":["go","golang","rpc","rpc-framework","streaming"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/pojntfx.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-11-10T20:59:44.000Z","updated_at":"2025-04-12T15:46:20.000Z","dependencies_parsed_at":"2023-10-25T22:29:58.337Z","dependency_job_id":"a74e9e4d-cb1b-4782-b426-a07cad42847f","html_url":"https://github.com/pojntfx/panrpc","commit_stats":{"total_commits":31,"total_committers":1,"mean_commits":31.0,"dds":0.0,"last_synced_commit":"4cb0a9be44dcd4861931d396af291141de1dc842"},"previous_names":["pojntfx/dudirekta","pojntfx/panrpc"],"tags_count":20,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pojntfx%2Fpanrpc","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pojntfx%2Fpanrpc/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pojntfx%2Fpanrpc/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pojntfx%2Fpanrpc/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/pojntfx","download_url":"https://codeload.github.com/pojntfx/panrpc/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":249766241,"owners_count":21322563,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["go","golang","rpc","rpc-framework","streaming"],"created_at":"2024-12-12T08:12:38.530Z","updated_at":"2025-04-19T18:37:59.242Z","avatar_url":"https://github.com/pojntfx.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cimg alt=\"Project icon\" style=\"vertical-align: middle;\" src=\"./docs/icon.svg\" width=\"128\" height=\"128\" align=\"left\"\u003e\n\n# panrpc\n\nLanguage-, transport- and serialization-agnostic RPC framework with remote closure support that allows exposing and calling functions on both clients and servers.\n\n[![hydrun CI](https://github.com/pojntfx/panrpc/actions/workflows/hydrun.yaml/badge.svg)](https://github.com/pojntfx/panrpc/actions/workflows/hydrun.yaml)\n![Go Version](https://img.shields.io/badge/go%20version-%3E=1.18-61CFDD.svg)\n[![Go Reference](https://pkg.go.dev/badge/github.com/pojntfx/panrpc/go.svg)](https://pkg.go.dev/github.com/pojntfx/panrpc/go)\n[![npm CI](https://github.com/pojntfx/panrpc/actions/workflows/npm.yaml/badge.svg)](https://github.com/pojntfx/panrpc/actions/workflows/npm.yaml)\n[![npm: @pojntfx/panrpc](https://img.shields.io/npm/v/@pojntfx/panrpc)](https://www.npmjs.com/package/@pojntfx/panrpc)\n[![TypeScript docs](https://img.shields.io/badge/TypeScript%20-docs-blue.svg)](https://pojntfx.github.io/panrpc)\n[![Matrix](https://img.shields.io/matrix/panrpc:matrix.org)](https://matrix.to/#/#panrpc:matrix.org?via=matrix.org)\n\n## Overview\n\npanrpc 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.\n\nIt enables you to:\n\n- **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](#protocol) means that [multiple languages](#examples) are supported and adding support for new ones is simple.\n- **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](https://pkg.go.dev/github.com/pojntfx/panrpc/go/pkg/rpc#LinkStream) or a [message](https://pkg.go.dev/github.com/pojntfx/panrpc/go/pkg/rpc#LinkMessage), meaning it works over everything from TCP, WebSockets, UNIX sockets, WebRTC, Valkey/Redis, NATS and more.\n- **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.\n- **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.\n- **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.\n\n## Installation\n\n### Library\n\nYou can add panrpc to your \u003cimg alt=\"Go\" src=\"https://cdn.simpleicons.org/go\" style=\"vertical-align: middle;\" height=\"20\" width=\"20\" /\u003e **Go** project by running the following:\n\n```shell\n$ go get github.com/pojntfx/panrpc/go/...@latest\n```\n\nFor \u003cimg alt=\"typescript\" src=\"https://cdn.simpleicons.org/typescript\" style=\"vertical-align: middle;\" height=\"20\" width=\"20\" /\u003e **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:\n\n```shell\n$ npm install @pojntfx/panrpc\n```\n\n### `purl` Tool\n\nIn addition to the library, the CLI tool `purl` is also available; `purl` is like [cURL](https://curl.se/) and [gRPCurl](https://github.com/fullstorydev/grpcurl), but for panrpc: A command-line tool for interacting with panrpc servers. `purl` is provided in the form of static binaries.\n\nOn Linux, you can install them like so:\n\n```shell\n$ curl -L -o /tmp/purl \"https://github.com/pojntfx/panrpc/releases/latest/download/purl.linux-$(uname -m)\"\n$ sudo install /tmp/purl /usr/local/bin\n```\n\nOn macOS, you can use the following:\n\n```shell\n$ curl -L -o /tmp/purl \"https://github.com/pojntfx/panrpc/releases/latest/download/purl.darwin-$(uname -m)\"\n$ sudo install /tmp/purl /usr/local/bin\n```\n\nOn Windows, the following should work (using PowerShell as administrator):\n\n```PowerShell\nInvoke-WebRequest https://github.com/pojntfx/panrpc/releases/latest/download/purl.windows-x86_64.exe -OutFile \\Windows\\System32\\purl.exe\n```\n\nYou can find binaries for more operating systems and architectures on [GitHub releases](https://github.com/pojntfx/panrpc/releases).\n\n## Tutorial\n\n### \u003cimg alt=\"Go\" src=\"https://cdn.simpleicons.org/go\" style=\"vertical-align: middle;\" height=\"20\" width=\"20\" /\u003e Go\n\n\u003e Just looking for sample code? Check out the sources for the example [coffee machine server](./go/cmd/panrpc-example-websocket-coffee-server-cli/main.go) and [coffee machine client/remote control](./go/cmd/panrpc-example-websocket-coffee-client-cli/main.go).\n\n#### 1. Choosing a Transport and Serializer\n\n\u003cdetails\u003e\n  \u003csummary\u003eExpand section\u003c/summary\u003e\n\nStart by creating a new Go module for the tutorial and installing `github.com/pojntfx/panrpc/go`:\n\n```shell\n$ mkdir -p panrpc-tutorial-go\n$ cd panrpc-tutorial-go\n$ go mod init panrpc-tutorial-go\n$ go get github.com/pojntfx/panrpc/go/...@latest\n```\n\nThe 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`](https://pkg.go.dev/io#ReadWriter) can be used with the panrpc [`LinkStream` API](https://pkg.go.dev/github.com/pojntfx/panrpc/go/pkg/rpc#Registry.LinkStream). 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](https://pkg.go.dev/github.com/pojntfx/panrpc/go/pkg/rpc#Registry.LinkMessage) 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:\n\n```shell\n$ go get github.com/coder/websocket@latest\n```\n\nIn 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.\n\n\u003c/details\u003e\n\n#### 2. Creating a Server\n\nIn 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).\n\n\u003cdetails\u003e\n  \u003csummary\u003eExpand section\u003c/summary\u003e\n\nTo 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:\n\n```go\n// cmd/coffee-machine/main.go\n\npackage main\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"log\"\n\t\"slices\"\n\t\"time\"\n)\n\ntype coffeeMachine struct {\n\tsupportedVariants []string\n\twaterLevel        int\n}\n\nfunc (s *coffeeMachine) BrewCoffee(\n\tctx context.Context,\n\tvariant string,\n\tsize int,\n) (int, error) {\n\tif !slices.Contains(s.supportedVariants, variant) {\n\t\treturn 0, errors.New(\"unsupported variant\")\n\t}\n\n\tif s.waterLevel-size \u003c 0 {\n\t\treturn 0, errors.New(\"not enough water\")\n\t}\n\n\tlog.Println(\"Brewing coffee variant\", variant, \"in size\", size, \"ml\")\n\n\ttime.Sleep(time.Second * 5)\n\n\ts.waterLevel -= size\n\n\treturn s.waterLevel, nil\n}\n```\n\n\u003e The following limitations on which methods can be exposed as RPCs exist:\n\u003e\n\u003e - Methods must have `context.Context` as their first argument\n\u003e - Methods can not have variadic arguments\n\u003e - Methods must return either an error or a single value and an error\n\u003e - Methods must be public (private methods won't be callable as RPCs, but stay callable as regular methods)\n\nTo start turning the `BrewCoffee` method into an RPC, create an instance of the struct and pass it to a [panrpc Registry](https://pkg.go.dev/github.com/pojntfx/panrpc/go/pkg/rpc#Registry) like so:\n\n```go\n// cmd/coffee-machine/main.go\n\nimport \"github.com/pojntfx/panrpc/go/pkg/rpc\"\n\nfunc main() {\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\n\tservice := \u0026coffeeMachine{\n\t\tsupportedVariants: []string{\"latte\", \"americano\"},\n\t\twaterLevel:        1000,\n\t}\n\n\tvar clients atomic.Int64\n\tregistry := rpc.NewRegistry[struct{}, json.RawMessage](\n\t\tservice,\n\n\t\t\u0026rpc.RegistryHooks{\n\t\t\tOnClientConnect: func(remoteID string) {\n\t\t\t\tlog.Printf(\"%v remote controls connected\", clients.Add(1))\n\t\t\t},\n\t\t\tOnClientDisconnect: func(remoteID string) {\n\t\t\t\tlog.Printf(\"%v remote controls connected\", clients.Add(-1))\n\t\t\t},\n\t\t},\n\t)\n}\n```\n\nNow 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](#examples) to check out how you can set up a different transport (TCP, WebSockets, UNIX sockets etc.) and serializer (JSON, CBOR etc.) instead:\n\n\u003cdetails\u003e\n  \u003csummary\u003eExpand boilerplate code snippet\u003c/summary\u003e\n\n```go\n// cmd/coffee-machine/main.go\n\nimport (\n\t\"encoding/json\"\n\t\"net\"\n\t\"net/http\"\n\n\t\"github.com/pojntfx/panrpc/go/pkg/rpc\"\n\t\"github.com/coder/websocket\"\n)\n\nfunc main() {\n  // ...\n\n  // Create TCP listener\n\tlis, err := net.Listen(\"tcp\", \"127.0.0.1:1337\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tdefer lis.Close()\n\n\tlog.Println(\"Listening on\", lis.Addr())\n\n\t// Create HTTP server from TCP listener\n\tif err := http.Serve(lis, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tdefer func() {\n\t\t\tif err := recover(); err != nil {\n\t\t\t\tw.WriteHeader(http.StatusInternalServerError)\n\n\t\t\t\tlog.Printf(\"Remote control disconnected with error: %v\", err)\n\t\t\t}\n\t\t}()\n\n\t\t// Upgrade from HTTP to WebSockets\n\t\tswitch r.Method {\n\t\tcase http.MethodGet:\n\t\t\tc, err := websocket.Accept(w, r, \u0026websocket.AcceptOptions{\n\t\t\t\tOriginPatterns: []string{\"*\"},\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\tpanic(err)\n\t\t\t}\n\n\t\t\tpings := time.NewTicker(time.Second / 2)\n\t\t\tdefer pings.Stop()\n\n\t\t\terrs := make(chan error)\n\t\t\tgo func() {\n\t\t\t\tfor range pings.C {\n\t\t\t\t\tif err := c.Ping(ctx); err != nil {\n\t\t\t\t\t\terrs \u003c- err\n\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}()\n\n\t\t\tconn := websocket.NetConn(ctx, c, websocket.MessageText)\n\t\t\tdefer conn.Close()\n\n\t\t\t// Set up the streaming JSON encoder and decoder\n\t\t\tencoder := json.NewEncoder(conn)\n\t\t\tdecoder := json.NewDecoder(conn)\n\n\t\t\tgo func() {\n\t\t\t\tif err := registry.LinkStream(\n          r.Context(),\n\n\t\t\t\t\tfunc(v rpc.Message[json.RawMessage]) error {\n\t\t\t\t\t\treturn encoder.Encode(v)\n\t\t\t\t\t},\n\t\t\t\t\tfunc(v *rpc.Message[json.RawMessage]) error {\n\t\t\t\t\t\treturn decoder.Decode(v)\n\t\t\t\t\t},\n\n\t\t\t\t\tfunc(v any) (json.RawMessage, error) {\n\t\t\t\t\t\tb, err := json.Marshal(v)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn nil, err\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\treturn json.RawMessage(b), nil\n\t\t\t\t\t},\n\t\t\t\t\tfunc(data json.RawMessage, v any) error {\n\t\t\t\t\t\treturn json.Unmarshal([]byte(data), v)\n\t\t\t\t\t},\n\t\t\t\t); err != nil {\n\t\t\t\t\terrs \u003c- err\n\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}()\n\n\t\t\tif err := \u003c-errs; err != nil {\n\t\t\t\tpanic(err)\n\t\t\t}\n\t\tdefault:\n\t\t\tw.WriteHeader(http.StatusMethodNotAllowed)\n\t\t}\n\t})); err != nil {\n\t\tpanic(err)\n\t}\n}\n```\n\n\u003c/details\u003e\n\n**Congratulations!** You've created your first panrpc server. You can start it from your terminal like so:\n\n```shell\n$ go run ./cmd/coffee-machine/main.go\n```\n\nYou should now see the following in your terminal, which means that the server is available on `localhost:1337`:\n\n```plaintext\nListening on localhost:1337\n```\n\n\u003c/details\u003e\n\n#### 3. Creating a Client\n\nIn order to interact with the coffee machine server, we'll now create the remote control (the coffee machine client), which will call the `BrewCoffee` RPC.\n\n\u003cdetails\u003e\n  \u003csummary\u003eExpand section\u003c/summary\u003e\n\nTo start with implementing the remote control, create a new file `cmd/remote-control/main.go` and define a basic struct with a placeholder method that mirrors the `BrewCoffee` RPC:\n\n```go\n// cmd/remote-control/main.go\n\npackage main\n\nimport \"context\"\n\ntype coffeeMachine struct {\n\tBrewCoffee func(\n\t\tctx context.Context,\n\t\tvariant string,\n\t\tsize int,\n\t) (int, error)\n}\n```\n\nIn order to make the `BrewCoffee` placeholder method do RPC calls, create an instance of the struct and pass it to a [panrpc Registry](https://pkg.go.dev/github.com/pojntfx/panrpc/go/pkg/rpc#Registry) like so:\n\n```go\n// cmd/remote-control/main.go\n\nimport \"github.com/pojntfx/panrpc/go/pkg/rpc\"\n\nfunc main() {\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\n\tvar clients atomic.Int64\n\tregistry := rpc.NewRegistry[coffeeMachine, json.RawMessage](\n\t\t\u0026struct{}{},\n\n\t\t\u0026rpc.RegistryHooks{\n\t\t\tOnClientConnect: func(remoteID string) {\n\t\t\t\tlog.Printf(\"%v coffee machines connected\", clients.Add(1))\n\t\t\t},\n\t\t\tOnClientDisconnect: func(remoteID string) {\n\t\t\t\tlog.Printf(\"%v coffee machines connected\", clients.Add(-1))\n\t\t\t},\n\t\t},\n\t)\n}\n```\n\nNow that we have a registry that turns the remote control's placeholder methods into RPC calls, we can link it to our transport (WebSockets) and serializer of choice (JSON). Once again, this requires a bit of boilerplate to connect to the WebSocket, so feel free to copy-and-paste this, or take a look at the [examples](#examples) to check out how you can set up a different transport (TCP, WebSockets, UNIX sockets etc.) and serializer (JSON, CBOR etc.) instead:\n\n\u003cdetails\u003e\n  \u003csummary\u003eExpand boilerplate code snippet\u003c/summary\u003e\n\n```go\n// cmd/remote-control/main.go\n\nimport (\n\t\"encoding/json\"\n\n\t\"github.com/pojntfx/panrpc/go/pkg/rpc\"\n\t\"github.com/coder/websocket\"\n)\n\nfunc main() {\n  // ...\n\n // Connect to WebSocket server\n\tc, _, err := websocket.Dial(ctx, \"ws://127.0.0.1:1337\", nil)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tconn := websocket.NetConn(ctx, c, websocket.MessageText)\n\tdefer conn.Close()\n\n\tlog.Println(\"Connected to localhost:1337\")\n\n\t// Set up the streaming JSON encoder and decoder\n\tencoder := json.NewEncoder(conn)\n\tdecoder := json.NewDecoder(conn)\n\n\tif err := registry.LinkStream(\n    ctx,\n\n\t\tfunc(v rpc.Message[json.RawMessage]) error {\n\t\t\treturn encoder.Encode(v)\n\t\t},\n\t\tfunc(v *rpc.Message[json.RawMessage]) error {\n\t\t\treturn decoder.Decode(v)\n\t\t},\n\n\t\tfunc(v any) (json.RawMessage, error) {\n\t\t\tb, err := json.Marshal(v)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\treturn json.RawMessage(b), nil\n\t\t},\n\t\tfunc(data json.RawMessage, v any) error {\n\t\t\treturn json.Unmarshal([]byte(data), v)\n\t\t},\n\t); err != nil {\n\t\tpanic(err)\n\t}\n}\n```\n\n\u003c/details\u003e\n\n**Cheers!** You've created your first panrpc client. You can start it from your terminal like so:\n\n```shell\n$ go run ./cmd/remote-control/main.go\n```\n\nYou should now see the following in your terminal, which means that the client has connected to the panrpc server at `localhost:1337`:\n\n```plaintext\nConnected to localhost:1337\n1 coffee machines connected\n```\n\nSimilarly so, the coffee machine server should output the following:\n\n```plaintext\n1 remote controls connected\n```\n\n\u003c/details\u003e\n\n#### 4. Calling the Server's RPCs from the Client\n\nThe coffee machine and the client are now connected to each other, but we haven't added the ability to call the `BrewCoffee` RPC from the remote control just yet. To fix this, we'll create a simple TUI interface that will print a list of available coffee variants and sizes to the terminal, waits for the user to make their choice by entering a number, and then calls the `BrewCoffee` RPC with the correct arguments. After the coffee has been brewed, we'll print the new water level to the terminal.\n\n\u003cdetails\u003e\n  \u003csummary\u003eExpand section\u003c/summary\u003e\n\nTo achieve this, we can call this RPC transparently from the remote control by accessing the connected coffee machine(s) with `registry.ForRemotes`, and we can handle errors by checking with `if err := ..., err != nil { ... }` just like if we were making a local function call:\n\n```go\n// cmd/remote-control/main.go\n\nimport (\n\t\"bufio\"\n\t\"log\"\n\t\"os\"\n)\n\nfunc main() {\n  // ...\n\n  go func() {\n\t\tlog.Println(`Enter one of the following numbers followed by \u003cENTER\u003e to brew a coffee:\n\n- 1: Brew small Cafè Latte\n- 2: Brew large Cafè Latte\n\n- 3: Brew small Americano\n- 4: Brew large Americano`)\n\n\t\tstdin := bufio.NewReader(os.Stdin)\n\n\t\tfor {\n\t\t\tline, err := stdin.ReadString('\\n')\n\t\t\tif err != nil {\n\t\t\t\tpanic(err)\n\t\t\t}\n\n\t\t\tif err := registry.ForRemotes(func(remoteID string, remote coffeeMachine) error {\n\t\t\t\tswitch line {\n\t\t\t\tcase \"1\\n\":\n\t\t\t\t\tfallthrough\n\t\t\t\tcase \"2\\n\":\n\t\t\t\t\tres, err := remote.BrewCoffee(\n\t\t\t\t\t\tctx,\n\t\t\t\t\t\t\"latte\",\n\t\t\t\t\t\tfunc() int {\n\t\t\t\t\t\t\tif line == \"1\" {\n\t\t\t\t\t\t\t\treturn 100\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\treturn 200\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}(),\n\t\t\t\t\t)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tlog.Println(\"Couldn't brew Cafè Latte:\", err)\n\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t}\n\n\t\t\t\t\tlog.Println(\"Remaining water:\", res, \"ml\")\n\n\t\t\t\tcase \"3\\n\":\n\t\t\t\t\tfallthrough\n\t\t\t\tcase \"4\\n\":\n\t\t\t\t\tres, err := remote.BrewCoffee(\n\t\t\t\t\t\tctx,\n\t\t\t\t\t\t\"americano\",\n\t\t\t\t\t\tfunc() int {\n\t\t\t\t\t\t\tif line == \"1\" {\n\t\t\t\t\t\t\t\treturn 100\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\treturn 200\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}(),\n\t\t\t\t\t)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tlog.Println(\"Couldn't brew Americano:\", err)\n\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t}\n\n\t\t\t\t\tlog.Println(\"Remaining water:\", res, \"ml\")\n\n\t\t\t\tdefault:\n\t\t\t\t\tlog.Printf(\"Unknown letter %v, ignoring input\", line)\n\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\treturn nil\n\t\t\t}); err != nil {\n\t\t\t\tpanic(err)\n\t\t\t}\n\t\t}\n\t}()\n\n  // ...\n}\n```\n\n\u003e Note that by cancelling the `context.Context` that we pass in as the first argument to every RPC call, you can cancel an RPC call before it has returned, which is useful for [implementing things like timeouts](https://pkg.go.dev/context#WithTimeout). If you don't cancel this `context.Context` like we do in this example, the RPC call will simply block until it returns.\n\nNow we can restart the remote control like so:\n\n```shell\n$ go run ./cmd/remote-control/main.go\n```\n\nAfter which you should see the following output:\n\n```plaintext\nEnter one of the following numbers followed by \u003cENTER\u003e to brew a coffee:\n\n- 1: Brew small Cafè Latte\n- 2: Brew large Cafè Latte\n\n- 3: Brew small Americano\n- 4: Brew large Americano\n1 coffee machines connected\nConnected to localhost:1337\n```\n\nIt is now possible to brew a coffee by pressing a number and \u003ckbd\u003eENTER\u003c/kbd\u003e. Once the RPC has been called, the coffee machine should print something like the following:\n\n```plaintext\nBrewing coffee variant latte in size 100 ml\n```\n\nAnd after the coffee has been brewed, the remote control should return the remaining water level like so:\n\n```plaintext\nRemaining water: 900 ml\n```\n\n**Enjoy your (virtual) coffee!** You've successfully called an RPC provided by a server from the client. Feel free to try out the other supported variants and sizes until there is no more water remaining.\n\n\u003c/details\u003e\n\n#### 5. Calling the Client's RPCs from the Server\n\nSo far, we've enabled a remote control/client to call the `BrewCoffee` RPC on the coffee machine/server. This however means that if multiple remote controls are connected to one coffee machine, only the remote control that called the RPC is aware of coffee being brewed. In order to notify the other remote controls that coffee is being brewed, we will use panrpc to call a new RPC on the remote control/client from the coffee machine/server each time we brew coffee.\n\n\u003cdetails\u003e\n  \u003csummary\u003eExpand section\u003c/summary\u003e\n\nTo get started, we can once again create a basic struct on the client with a method `SetCoffeeMachineBrewing`, which will print the state of the coffee machine to the remote control's terminal:\n\n```go\n// cmd/remote-control/main.go\n\ntype remoteControl struct{}\n\nfunc (s *remoteControl) SetCoffeeMachineBrewing(ctx context.Context, brewing bool) error {\n\tif brewing {\n\t\tlog.Println(\"Coffee machine is now brewing\")\n\t} else {\n\t\tlog.Println(\"Coffee machine has stopped brewing\")\n\t}\n\n\treturn nil\n}\n```\n\nTo start turning this new `SetCoffeeMachineBrewing` method into an RPC that server can call, create an instance of the struct and pass it to the client's registry like so:\n\n```go\n// cmd/remote-control/main.go\n\nfunc main() {\n  // ...\n\n  registry := rpc.NewRegistry[coffeeMachine, json.RawMessage](\n\t\t\u0026remoteControl{},\n\n\t\t\u0026rpc.RegistryHooks{\n\t\t\tOnClientConnect: func(remoteID string) {\n\t\t\t\tlog.Printf(\"%v coffee machines connected\", clients.Add(1))\n\t\t\t},\n\t\t\tOnClientDisconnect: func(remoteID string) {\n\t\t\t\tlog.Printf(\"%v coffee machines connected\", clients.Add(-1))\n\t\t\t},\n\t\t},\n\t)\n\n  // ...\n}\n```\n\nThe remote control/client now exposes the `SetCoffeeMachineBrewing` RPC, and we can start enabling the coffee machine/server to call it by defining a basic struct with a method that mirrors the RPC, just like we did before on the remote control for `BrewCoffee`:\n\n```go\n// cmd/coffee-machine/main.go\n\ntype remoteControl struct {\n\tSetCoffeeMachineBrewing func(ctx context.Context, brewing bool) error\n}\n```\n\nIn order to make the `SetCoffeeMachineBrewing` placeholder method do RPC calls, create an instance of the struct and pass it to the server's registry like so:\n\n```go\n// cmd/coffee-machine/main.go\n\nfunc main() {\n  // ...\n\n\tregistry := rpc.NewRegistry[remoteControl, json.RawMessage](\n\t\tservice,\n\n\t\t\u0026rpc.RegistryHooks{\n\t\t\tOnClientConnect: func(remoteID string) {\n\t\t\t\tlog.Printf(\"%v remote controls connected\", clients.Add(1))\n\t\t\t},\n\t\t\tOnClientDisconnect: func(remoteID string) {\n\t\t\t\tlog.Printf(\"%v remote controls connected\", clients.Add(-1))\n\t\t\t},\n\t\t},\n\t)\n\n  // ...\n}\n```\n\nThe coffee machine/server and the remote control/client now both know of the new `SetCoffeeMachineBrewing` RPC, but the server doesn't call it yet. To fix this, we can call this RPC transparently from the coffee machine by accessing the connected remote control(s) with `registry.ForRemotes` just like we did before in the remote control, and we can handle errors by checking with `if err := ..., err != nil { ... }` just like if we were making a local function call. We'll also use the first argument to the RPC, `ctx`, in conjunction with `rpc.GetRemoteID` to get the ID of the remote control/client that is calling `BrewCoffee`, so that we don't call `SetCoffeeMachineBrewing` on the remote control/client that is calling `BrewCoffee` itself:\n\n```go\n// cmd/remote-control/main.go\n\ntype coffeeMachine struct {\n\tsupportedVariants []string\n\twaterLevel        int\n\n\tForRemotes func(\n\t\tcb func(remoteID string, remote remoteControl) error,\n\t) error\n}\n\nfunc (s *coffeeMachine) BrewCoffee(\n\tctx context.Context,\n\tvariant string,\n\tsize int,\n) (int, error) {\n  // Get the ID of the remote control that's calling `BrewCoffee`\n\ttargetID := rpc.GetRemoteID(ctx)\n\n  // Notify connected remote controls that coffee is no longer brewing\n\tdefer s.ForRemotes(func(remoteID string, remote remoteControl) error {\n    // Don't call `SetCoffeeMachineBrewing` if it's the remote control that's calling `BrewCoffee`\n\t\tif remoteID == targetID {\n\t\t\treturn nil\n\t\t}\n\n\t\treturn remote.SetCoffeeMachineBrewing(ctx, false)\n\t})\n\n  // Notify connected remote controls that coffee is brewing\n\tif err := s.ForRemotes(func(remoteID string, remote remoteControl) error {\n    // Don't call `SetCoffeeMachineBrewing` if it's the remote control that's calling `BrewCoffee`\n\t\tif remoteID == targetID {\n\t\t\treturn nil\n\t\t}\n\n\t\treturn remote.SetCoffeeMachineBrewing(ctx, true)\n\t}); err != nil {\n\t\treturn 0, err\n\t}\n\n\tif !slices.Contains(s.supportedVariants, variant) {\n\t\treturn 0, errors.New(\"unsupported variant\")\n\t}\n\n\tif s.waterLevel-size \u003c 0 {\n\t\treturn 0, errors.New(\"not enough water\")\n\t}\n\n\tlog.Println(\"Brewing coffee variant\", variant, \"in size\", size, \"ml\")\n\n\ttime.Sleep(time.Second * 5)\n\n\ts.waterLevel -= size\n\n\treturn s.waterLevel, nil\n}\n```\n\nNote that we've added the `forRemotes` field to the coffee machine/server; we can get the implementation for it from the registry like so:\n\n```go\n// cmd/coffee-machine/main.go\n\nfunc main{\n  service := // ...\n\n  registry := // ...\n  service.forRemotes = registry.ForRemotes;\n}\n```\n\nNow that we've added support for this RPC to the coffee machine/server, we can restart it like so:\n\n```shell\n$ go run ./cmd/coffee-machine/main.go\n```\n\nTo test if it works, connect two remote controls/clients to it like so:\n\n```shell\n$ go run ./cmd/remote-control/main.go\n# In another terminal\n$ go run ./cmd/remote-control/main.go\n```\n\nYou can now request the coffee machine to brew a coffee on either of the remote controls by pressing a number and \u003ckbd\u003eENTER\u003c/kbd\u003e. Once the RPC has been called, the coffee machine should print something like the following again:\n\n```plaintext\nBrewing coffee variant latte in size 100 ml\n```\n\nAnd after the coffee has been brewed, the remote control that you've chosen to brew the coffee with should once again return the remaining water level like so:\n\n```plaintext\nRemaining water: 900 ml\n```\n\nThe other connected remote controls will be notified that the coffee machine is brewing, and then once it has finished brewing:\n\n```plaintext\nCoffee machine is now brewing\nCoffee machine has stopped brewing\n```\n\n**Enjoy your distributed coffee machine!** You've successfully called an RPC provided by a client from the server to implement multicast notifications, something that usually is quite complex to do with RPC systems.\n\n\u003c/details\u003e\n\n#### 6. Passing Closures to RPCs\n\nSo far, when the remote control/client calls the `BrewCoffee` RPC, there is no way of knowing the incremental progress of the brew other than waiting for `BrewCoffee` to return the new water level. In order to know of the progress of the coffee machine as it is brewing, we can make use of the closure/callback support in panrpc, which allows us to pass a function to an RPC call, just like you could do locally.\n\n\u003cdetails\u003e\n  \u003csummary\u003eExpand section\u003c/summary\u003e\n\nFirst, we'll add a `onProgress` callback to the coffee machine's `BrewCoffee` implementation, which we then call incrementally during the brewing process:\n\n```go\n// cmd/coffee-machine/main.go\n\nfunc (s *coffeeMachine) BrewCoffee(\n\tctx context.Context,\n\tvariant string,\n\tsize int,\n\tonProgress func(ctx context.Context, percentage int) error, // This is new\n) (int, error) {\n\t// ...\n\n\t// Report 0% brewing process\n\tif err := onProgress(ctx, 0); err != nil {\n\t\treturn 0, err\n\t}\n\n\t// Report 25% brewing process\n\ttime.Sleep(500 * time.Millisecond)\n\tif err := onProgress(ctx, 25); err != nil {\n\t\treturn 0, err\n\t}\n\n\t// Report 50% brewing process\n\ttime.Sleep(500 * time.Millisecond)\n\tif err := onProgress(ctx, 50); err != nil {\n\t\treturn 0, err\n\t}\n\n\t// Report 75% brewing process\n\ttime.Sleep(500 * time.Millisecond)\n\tif err := onProgress(ctx, 75); err != nil {\n\t\treturn 0, err\n\t}\n\n\t// Report 100% brewing process\n\ttime.Sleep(500 * time.Millisecond)\n\tif err := onProgress(ctx, 100); err != nil {\n\t\treturn 0, err\n\t}\n\n\t// ..\n\n\treturn s.waterLevel, nil\n}\n```\n\nIn the remote control, we'll also extend the struct with the `BrewCoffee` placeholder method with this new RPC argument:\n\n```go\n// cmd/remote-control/main.go\n\ntype coffeeMachine struct {\n\tBrewCoffee func(\n\t\tctx context.Context,\n\t\tvariant string,\n\t\tsize int,\n\t\tonProgress func(ctx context.Context, percentage int) error, // This is new\n\t) (int, error)\n}\n```\n\nAnd finally, where we call the `BrewCoffee` RPC in the remote control/client, we can pass in the implementation of this closure:\n\n```go\n// cmd/remote-control/main.go\n\ngo func() {\n\t// ...\n\tfor {\n\t\t// ...\n\t\tif err := registry.ForRemotes(func(remoteID string, remote coffeeMachine) error {\n\t\t\tswitch line {\n\t\t\tcase \"1\\n\":\n\t\t\t\tfallthrough\n\t\t\tcase \"2\\n\":\n\t\t\t\tres, err := remote.BrewCoffee(\n\t\t\t\t\tctx,\n\t\t\t\t\t\"latte\",\n\t\t\t\t\tfunc() int {\n\t\t\t\t\t\tif line == \"1\" {\n\t\t\t\t\t\t\treturn 100\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\treturn 200\n\t\t\t\t\t\t}\n\t\t\t\t\t}(),\n\t\t\t\t\tfunc(ctx context.Context, percentage int) error {\n\t\t\t\t\t\tlog.Printf(`Brewing Cafè Latte ... %v%% done`, percentage) // This is new\n\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t)\n\n\t\t\t\t// ...\n\n\t\t\tcase \"3\\n\":\n\t\t\t\tfallthrough\n\t\t\tcase \"4\\n\":\n\t\t\t\tres, err := remote.BrewCoffee(\n\t\t\t\t\tctx,\n\t\t\t\t\t\"americano\",\n\t\t\t\t\tfunc() int {\n\t\t\t\t\t\tif line == \"1\" {\n\t\t\t\t\t\t\treturn 100\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\treturn 200\n\t\t\t\t\t\t}\n\t\t\t\t\t}(),\n\t\t\t\t\tfunc(ctx context.Context, percentage int) error {\n\t\t\t\t\t\tlog.Printf(`Brewing Americano ... %v%% done`, percentage) // This is new\n\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t)\n\n\t\t\t\t// ...\n\n\t\t\treturn nil\n\t\t}); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}\n}()\n```\n\nNow we can restart the coffee machine/server again like so:\n\n```shell\n$ go run ./cmd/coffee-machine/main.go\n```\n\nAnd connect the remote control/client to it again like so:\n\n```shell\n$ go run ./cmd/remote-control/main.go\n```\n\nYou can now request the coffee machine to brew a coffee by pressing a number and \u003ckbd\u003eENTER\u003c/kbd\u003e. Once the RPC has been called, the coffee machine should print something like the following again:\n\n```plaintext\nBrewing coffee variant latte in size 100 ml\n```\n\nAnd the remote control will print the progress as reported by the coffee machine to the terminal, before once again returning the remaining water level like so:\n\n```plaintext\nBrewing Cafè Latte ... 0% done\nBrewing Cafè Latte ... 25% done\nBrewing Cafè Latte ... 50% done\nBrewing Cafè Latte ... 75% done\nBrewing Cafè Latte ... 100% done\nRemaining water: 900 ml\n```\n\n**Enjoy your live coffee brewing progress!** You've successfully implemented incremental coffee brewing progress reports by using panrpc's closure support, something that is usually quite tricky to do with RPC frameworks.\n\n\u003c/details\u003e\n\n#### 7. Nesting RPCs\n\nSo far, we've added RPCs directly to our coffee machine/server and remote control/client. While this approach is simple, it makes future extensions difficult. If we want to add more features, we would need to modify the coffee machine/server and remote control/client directly by adding new RPC methods, which can be hard to do in a type-safe way. Additionally, having only one level of RPCs makes large APIs hard to understand and organize as the number of RPCs increases.\n\nIn order to work around this, panrpc supports nesting RPCs in both clients and servers. This allows you to simplify top-level RPC calls; for example, instead of a single RPC like `GetConnectedChatUsers()`, you can use categorized, nested calls such as `Chat.Users.GetConnected()`.\n\n\u003cdetails\u003e\n  \u003csummary\u003eExpand section\u003c/summary\u003e\n\nTo define a nested RPC, simply add another struct as a public property to your existing coffee machine/server or remote control/client. In this new stuct, you can define RPCs as methods, just like with top-level RPCs. To call them, follow the same concept: Define placeholder methods in the new struct and add it as a public property to your coffee machine/server or remote control/client.\n\nIn this example, we'll add a tea brewer extension to our coffee machine/server and remote control/client. This extension will allow us to list available tea variants by calling the `Extension.GetVariants` RPC. For brevity, we won't implement all the tea brewing functions. To add the tea brewer extension to the coffee machine/server, first create a new `TeaBrewer` struct, similar to the `CoffeeMachine` struct. Then, add the `GetVariants` method to this class and instantiate it with the supported variants:\n\n```go\n// cmd/coffee-machine/main.go\n\ntype teaBrewer struct {\n\tsupportedVariants []string\n}\n\nfunc (s *teaBrewer) GetVariants(ctx context.Context) ([]string, error) {\n\treturn s.supportedVariants, nil\n}\n```\n\nTo add these nested RPCs to the main `CoffeeMachine` struct, we'll include them as a public property in the struct. To keep the coffee machine flexible and avoid depending on the `TeaBrewer` extension, we'll name the property `Extension` and make it generic in `CoffeeMachine`. This way, we can easily replace the tea brewer with another extension in the future, such as a full-featured tea brewer:\n\n```go\n// cmd/coffee-machine/main.go\n\ntype coffeeMachine[E any] struct {\n\tExtension E\n\n  // ...\n}\n```\n\nFinally, for the coffee machine/server, create an instance of our extension and pass it to the `CoffeeMachine` when initializing it. This is also where you provide the list of supported tea variants:\n\n```go\n// cmd/coffee-machine/main.go\n\n// ...\nservice := \u0026coffeeMachine[*teaBrewer]{\n\t\tExtension: \u0026teaBrewer{\n\t\t\tsupportedVariants: []string{\"darjeeling\", \"chai\", \"earlgrey\"},\n\t\t},\n\n\t\tsupportedVariants: []string{\"latte\", \"americano\"},\n\t\twaterLevel:        1000,\n\t}\n// ...\n```\n\nFor the remote control/client, it's a very similar process. First, we define the new `TeaBrewer` struct with the `GetVariants` placeholder method:\n\n```go\n// cmd/remote-control/main.go\n\ntype teaBrewer struct {\n\tGetVariants func(ctx context.Context) ([]string, error)\n}\n```\n\nThen we'll add the nested RPCs to the main `CoffeeMachine` struct as a public instance property, and we'll use generics again to keep everything extensible:\n\n```go\n// cmd/remote-control/main.go\n\ntype coffeeMachine[E any] struct {\n\tExtension E\n\n\t// ...\n}\n```\n\nAfter this, we need to create an instance of our extension and pass it to `CoffeeMachine` when we create it:\n\n```go\n// cmd/remote-control/main.go\n\nregistry := rpc.NewRegistry[coffeeMachine[teaBrewer], json.RawMessage](\n\t\t\u0026remoteControl{},\n\n\t\t// ...\n\t)\n```\n\nAnd finally, we add another switch case to the remote control/client so that we can call `Extension.GetVariants`:\n\n```go\n// cmd/remote-control/main.go\n\n\tgo func() {\n\t\tlog.Println(`Enter one of the following numbers followed by \u003cENTER\u003e to brew a coffee:\n\n- 1: Brew small Cafè Latte\n- 2: Brew large Cafè Latte\n\n- 3: Brew small Americano\n- 4: Brew large Americano\n\nOr enter 5 to list available tea variants.`)\n\n  // ...\n\n  if err := registry.ForRemotes(func(remoteID string, remote coffeeMachine[teaBrewer]) error {\n\t\tswitch line {\n      // ..\n      case \"5\\n\":\n\t\t\t\tres, err := remote.Extension.GetVariants(ctx)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Println(\"Couldn't list available tea variants:\", err)\n\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tlog.Println(\"Available tea variants:\", res)\n\n      default:\n      // ..\n\t\t}\n\t}\n}()\n```\n\nNow we can restart the coffee machine/server again like so:\n\n```shell\n$ go run ./cmd/coffee-machine/main.go\n```\n\nAnd connect the remote control/client to it again like so:\n\n```shell\n$ go run ./cmd/remote-control/main.go\n```\n\nYou can now request the coffee machine to list the available tea variants by pressing `5` and \u003ckbd\u003eENTER\u003c/kbd\u003e. Once the RPC has been called, the remote control should print something like the following:\n\n```plaintext\nAvailable tea variants: [ \"darjeeling\", \"chai\", \"earlgrey\" ]\n```\n\n**🚀 That's it!** You've successfully built a virtual coffee machine with support for brewing coffee, notifications when coffee is being brewed, and incremental coffee brewing progress reports. You've also made it easily extensible by using nested RPCs. We can't wait to see what you're going to build next with panrpc! Be sure to take a look at the [reference](#reference) and [examples](#examples) for more information, or check out the complete sources for the [coffee machine server](./go/cmd/panrpc-example-websocket-coffee-server-cli/main.go) and [coffee machine client/remote control](./go/cmd/panrpc-example-websocket-coffee-client-cli/main.go) for a recap.\n\n\u003c/details\u003e\n\n### \u003cimg alt=\"typescript\" src=\"https://cdn.simpleicons.org/typescript\" style=\"vertical-align: middle;\" height=\"20\" width=\"20\" /\u003e TypeScript\n\n\u003e Just looking for sample code? Check out the sources for the example [coffee machine server](./ts/bin/panrpc-example-websocket-coffee-server-cli.ts) and [coffee machine client/remote control](./ts/bin/panrpc-example-websocket-coffee-client-cli.ts).\n\n#### 1. Choosing a Transport and Serializer\n\n\u003cdetails\u003e\n  \u003csummary\u003eExpand section\u003c/summary\u003e\n\nStart by creating a new npm module for the tutorial and installing `@pojntfx/panrpc`:\n\n```shell\n$ mkdir -p panrpc-tutorial-typescript\n$ cd panrpc-tutorial-typescript\n$ npm init -y\n$ npm install @pojntfx/panrpc\n```\n\nThe TypeScript 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 [WHATWG stream](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API) can be used with the panrpc [`linkStream` API](https://pojntfx.github.io/panrpc/classes/Registry.html#linkStream). 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](https://pojntfx.github.io/panrpc/classes/Registry.html#linkMessage) instead. For this tutorial, we'll be using WebSockets as the transport through the `ws` library, which you can install like so:\n\n```shell\n$ npm install ws\n```\n\nIn addition to supporting many transports, the TypeScript 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 WHATWG stream can be used. For this tutorial, we'll be using JSON as the serializer through the `@streamparser/json-whatwg` library, which you can install like so:\n\n```shell\n$ npm install @streamparser/json-whatwg\n```\n\n\u003c/details\u003e\n\n#### 2. Creating a Server\n\nIn 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).\n\n\u003cdetails\u003e\n  \u003csummary\u003eExpand section\u003c/summary\u003e\n\nTo start with implementing the coffee machine server, create a new file `coffee-machine.ts` and define a basic class 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:\n\n```typescript\n// coffee-machine.ts\n\nimport { ILocalContext } from \"@pojntfx/panrpc\";\n\nclass CoffeeMachine {\n  #waterLevel: number;\n\n  constructor(private supportedVariants: string[], waterLevel: number) {\n    this.#waterLevel = waterLevel;\n\n    this.BrewCoffee = this.BrewCoffee.bind(this);\n  }\n\n  async BrewCoffee(\n    ctx: ILocalContext,\n    variant: string,\n    size: number\n  ): Promise\u003cnumber\u003e {\n    if (!this.supportedVariants.includes(variant)) {\n      throw new Error(\"unsupported variant\");\n    }\n\n    if (this.#waterLevel - size \u003c 0) {\n      throw new Error(\"not enough water\");\n    }\n\n    console.log(\"Brewing coffee variant\", variant, \"in size\", size, \"ml\");\n\n    await new Promise((r) =\u003e {\n      setTimeout(r, 5000);\n    });\n\n    this.#waterLevel -= size;\n\n    return this.#waterLevel;\n  }\n}\n```\n\n\u003e The following limitations on which methods can be exposed as RPCs exist:\n\u003e\n\u003e - Methods must have `ILocalContext` as their first argument\n\u003e - Methods can not have variadic arguments\n\u003e - Methods must be public (private methods - those that start with `#`, see [private properties](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Private_properties) - won't be callable as RPCs, but stay callable as regular methods)\n\nTo start turning the `BrewCoffee` method into an RPC, create an instance of the class and pass it to a [panrpc Registry](https://pojntfx.github.io/panrpc/classes/Registry.html) like so:\n\n```typescript\n// coffee-machine.ts\n\nimport { Registry } from \"@pojntfx/panrpc\";\n\nconst service = new CoffeeMachine([\"latte\", \"americano\"], 1000);\n\nlet clients = 0;\n\nconst registry = new Registry(\n  service,\n  new (class {})(),\n\n  {\n    onClientConnect: () =\u003e {\n      clients++;\n\n      console.log(clients, \"remote controls connected\");\n    },\n    onClientDisconnect: () =\u003e {\n      clients--;\n\n      console.log(clients, \"remote controls connected\");\n    },\n  }\n);\n```\n\nNow 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 since the `ws` library doesn't provide WHATWG streams directly yet, so feel free to copy-and-paste this, or take a look at the [examples](#examples) to check out how you can set up a different transport (TCP, WebSockets, UNIX sockets etc.) and serializer (JSON, CBOR etc.) instead:\n\n\u003cdetails\u003e\n  \u003csummary\u003eExpand boilerplate code snippet\u003c/summary\u003e\n\n```typescript\n// coffee-machine.ts\n\nimport { JSONParser } from \"@streamparser/json-whatwg\";\nimport { WebSocketServer } from \"ws\";\n\n// Create WebSocket server\nconst server = new WebSocketServer({\n  host: \"127.0.0.1\",\n  port: 1337,\n});\n\nserver.on(\"connection\", (socket) =\u003e {\n  socket.addEventListener(\"error\", (e) =\u003e {\n    console.error(\"Remote control disconnected with error:\", e);\n  });\n\n  const linkSignal = new AbortController();\n\n  // Set up streaming JSON encoder\n  const encoder = new WritableStream({\n    write(chunk) {\n      socket.send(JSON.stringify(chunk));\n    },\n  });\n\n  // Set up streaming JSON decoder\n  const parser = new JSONParser({\n    paths: [\"$\"],\n    separator: \"\",\n  });\n  const parserWriter = parser.writable.getWriter();\n  const parserReader = parser.readable.getReader();\n  const decoder = new ReadableStream({\n    start(controller) {\n      parserReader\n        .read()\n        .then(async function process({ done, value }) {\n          if (done) {\n            controller.close();\n\n            return;\n          }\n\n          controller.enqueue(value?.value);\n\n          parserReader\n            .read()\n            .then(process)\n            .catch((e) =\u003e controller.error(e));\n        })\n        .catch((e) =\u003e controller.error(e));\n    },\n  });\n  socket.addEventListener(\"message\", (m) =\u003e\n    parserWriter.write(m.data as string)\n  );\n  socket.addEventListener(\"close\", () =\u003e {\n    parserReader.cancel();\n    parserWriter.abort();\n    linkSignal.abort();\n  });\n\n  registry.linkStream(\n    linkSignal.signal,\n\n    encoder,\n    decoder,\n\n    (v) =\u003e v,\n    (v) =\u003e v\n  );\n});\n\nconsole.log(\"Listening on localhost:1337\");\n```\n\n\u003c/details\u003e\n\n**Congratulations!** You've created your first panrpc server. You can start it from your terminal like so:\n\n```shell\n$ npx tsx coffee-machine.ts\n```\n\nYou should now see the following in your terminal, which means that the server is available on `localhost:1337`:\n\n```plaintext\nListening on localhost:1337\n```\n\n\u003c/details\u003e\n\n#### 3. Creating a Client\n\nIn order to interact with the coffee machine server, we'll now create the remote control (the coffee machine client), which will call the `BrewCoffee` RPC.\n\n\u003cdetails\u003e\n  \u003csummary\u003eExpand section\u003c/summary\u003e\n\nTo start with implementing the remote control, create a new file `remote-control.ts` and define a basic class with a placeholder method that mirrors the `BrewCoffee` RPC:\n\n```typescript\n// remote-control.ts\n\nimport { IRemoteContext } from \"@pojntfx/panrpc\";\n\nclass CoffeeMachine {\n  async BrewCoffee(\n    ctx: IRemoteContext,\n    variant: string,\n    size: number\n  ): Promise\u003cnumber\u003e {\n    return 0;\n  }\n}\n```\n\n\u003e Placeholder methods must have `IRemoteContext` instead of `ILocalContext` as their first argument.\n\nIn order to make the `BrewCoffee` placeholder method do RPC calls, create an instance of the class and pass it to a [panrpc Registry](https://pojntfx.github.io/panrpc/classes/Registry.html) like so:\n\n```typescript\n// remote-control.ts\n\nimport { Registry } from \"@pojntfx/panrpc\";\n\nlet clients = 0;\n\nconst registry = new Registry(\n  new (class {})(),\n  new CoffeeMachine(),\n\n  {\n    onClientConnect: () =\u003e {\n      clients++;\n\n      console.log(clients, \"coffee machines connected\");\n    },\n    onClientDisconnect: () =\u003e {\n      clients--;\n\n      console.log(clients, \"coffee machines connected\");\n    },\n  }\n);\n```\n\nNow that we have a registry that turns the remote control's placeholder methods into RPC calls, we can link it to our transport (WebSockets) and serializer of choice (JSON). Once again, this requires a bit of boilerplate since the `ws` library doesn't provide WHATWG streams directly yet, so feel free to copy-and-paste this, or take a look at the [examples](#examples) to check out how you can set up a different transport (TCP, WebSockets, UNIX sockets etc.) and serializer (JSON, CBOR etc.) instead:\n\n\u003cdetails\u003e\n  \u003csummary\u003eExpand boilerplate code snippet\u003c/summary\u003e\n\n```typescript\n// remote-control.ts\n\nimport { JSONParser } from \"@streamparser/json-whatwg\";\nimport { WebSocket } from \"ws\";\n\n// Connect to WebSocket server\nconst socket = new WebSocket(\"ws://127.0.0.1:1337\");\n\nsocket.addEventListener(\"error\", (e) =\u003e {\n  console.error(\"Disconnected with error:\", e);\n\n  exit(1);\n});\nsocket.addEventListener(\"close\", () =\u003e exit(0));\n\nawait new Promise\u003cvoid\u003e((res, rej) =\u003e {\n  socket.addEventListener(\"open\", () =\u003e res());\n  socket.addEventListener(\"error\", rej);\n});\n\nconst linkSignal = new AbortController();\n\n// Set up streaming JSON encoder\nconst encoder = new WritableStream({\n  write(chunk) {\n    socket.send(JSON.stringify(chunk));\n  },\n});\n\n// Set up streaming JSON decoder\nconst parser = new JSONParser({\n  paths: [\"$\"],\n  separator: \"\",\n});\nconst parserWriter = parser.writable.getWriter();\nconst parserReader = parser.readable.getReader();\nconst decoder = new ReadableStream({\n  start(controller) {\n    parserReader\n      .read()\n      .then(async function process({ done, value }) {\n        if (done) {\n          controller.close();\n\n          return;\n        }\n\n        controller.enqueue(value?.value);\n\n        parserReader\n          .read()\n          .then(process)\n          .catch((e) =\u003e controller.error(e));\n      })\n      .catch((e) =\u003e controller.error(e));\n  },\n});\nsocket.addEventListener(\"message\", (m) =\u003e parserWriter.write(m.data as string));\nsocket.addEventListener(\"close\", () =\u003e {\n  parserReader.cancel();\n  parserWriter.abort();\n  linkSignal.abort();\n});\n\nregistry.linkStream(\n  linkSignal.signal,\n\n  encoder,\n  decoder,\n\n  (v) =\u003e v,\n  (v) =\u003e v\n);\n\nconsole.log(\"Connected to localhost:1337\");\n```\n\n\u003c/details\u003e\n\n**Cheers!** You've created your first panrpc client. You can start it from your terminal like so:\n\n```shell\n$ npx tsx remote-control.ts\n```\n\nYou should now see the following in your terminal, which means that the client has connected to the panrpc server at `localhost:1337`:\n\n```plaintext\nConnected to localhost:1337\n1 coffee machines connected\n```\n\nSimilarly so, the coffee machine server should output the following:\n\n```plaintext\n1 remote controls connected\n```\n\n\u003c/details\u003e\n\n#### 4. Calling the Server's RPCs from the Client\n\nThe coffee machine and the client are now connected to each other, but we haven't added the ability to call the `BrewCoffee` RPC from the remote control just yet. To fix this, we'll create a simple TUI interface that will print a list of available coffee variants and sizes to the terminal, waits for the user to make their choice by entering a number, and then calls the `BrewCoffee` RPC with the correct arguments. After the coffee has been brewed, we'll print the new water level to the terminal.\n\n\u003cdetails\u003e\n  \u003csummary\u003eExpand section\u003c/summary\u003e\n\nTo achieve this, we can call this RPC transparently from the remote control by accessing the connected coffee machine(s) with `registry.forRemotes`, and we can handle errors with `try catch` just like if we were making a local function call:\n\n```typescript\n// remote-control.ts\n\nimport { createInterface } from \"readline/promises\";\n\n(async () =\u003e {\n  console.log(`Enter one of the following numbers followed by \u003cENTER\u003e to brew a coffee:\n\n- 1: Brew small Cafè Latte\n- 2: Brew large Cafè Latte\n\n- 3: Brew small Americano\n- 4: Brew large Americano`);\n\n  const rl = createInterface({ input: stdin, output: stdout });\n\n  while (true) {\n    const line = await rl.question(\"\");\n\n    await registry.forRemotes(async (remoteID, remote) =\u003e {\n      switch (line) {\n        case \"1\":\n        case \"2\":\n          try {\n            const res = await remote.BrewCoffee(\n              undefined,\n              \"latte\",\n              line === \"1\" ? 100 : 200\n            );\n\n            console.log(\"Remaining water:\", res, \"ml\");\n          } catch (e) {\n            console.error(`Couldn't brew Cafè Latte: ${e}`);\n          }\n\n          break;\n\n        case \"3\":\n        case \"4\":\n          try {\n            const res = await remote.BrewCoffee(\n              undefined,\n              \"americano\",\n              line === \"3\" ? 100 : 200\n            );\n\n            console.log(\"Remaining water:\", res, \"ml\");\n          } catch (e) {\n            console.error(`Couldn't brew Americano: ${e}`);\n          }\n\n          break;\n\n        default:\n          console.log(`Unknown letter ${line}, ignoring input`);\n      }\n    });\n  }\n})();\n```\n\n\u003e Note that by aborting the [AbortSignal](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) that we can pass in as the first argument to every RPC call, you can abort an RPC call before it has returned, which is useful for [implementing things like timeouts](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal#aborting_a_fetch_operation_with_a_timeout). If you don't abort this `AbortSignal`, or pass in `undefined` like we do in this example, the RPC call will simply block until it returns.\n\nNow we can restart the remote control like so:\n\n```shell\n$ npx tsx remote-control.ts\n```\n\nAfter which you should see the following output:\n\n```plaintext\nEnter one of the following numbers followed by \u003cENTER\u003e to brew a coffee:\n\n- 1: Brew small Cafè Latte\n- 2: Brew large Cafè Latte\n\n- 3: Brew small Americano\n- 4: Brew large Americano\n1 coffee machines connected\nConnected to localhost:1337\n```\n\nIt is now possible to brew a coffee by pressing a number and \u003ckbd\u003eENTER\u003c/kbd\u003e. Once the RPC has been called, the coffee machine should print something like the following:\n\n```plaintext\nBrewing coffee variant latte in size 100 ml\n```\n\nAnd after the coffee has been brewed, the remote control should return the remaining water level like so:\n\n```plaintext\nRemaining water: 900 ml\n```\n\n**Enjoy your (virtual) coffee!** You've successfully called an RPC provided by a server from the client. Feel free to try out the other supported variants and sizes until there is no more water remaining.\n\n\u003c/details\u003e\n\n#### 5. Calling the Client's RPCs from the Server\n\nSo far, we've enabled a remote control/client to call the `BrewCoffee` RPC on the coffee machine/server. This however means that if multiple remote controls are connected to one coffee machine, only the remote control that called the RPC is aware of coffee being brewed. In order to notify the other remote controls that coffee is being brewed, we will use panrpc to call a new RPC on the remote control/client from the coffee machine/server each time we brew coffee.\n\n\u003cdetails\u003e\n  \u003csummary\u003eExpand section\u003c/summary\u003e\n\nTo get started, we can once again create a basic class on the client with a method `SetCoffeeMachineBrewing`, which will print the state of the coffee machine to the remote control's terminal:\n\n```typescript\n// remote-control.ts\n\nclass RemoteControl {\n  async SetCoffeeMachineBrewing(ctx: ILocalContext, brewing: boolean) {\n    if (brewing) {\n      console.log(\"Coffee machine is now brewing\");\n    } else {\n      console.log(\"Coffee machine has stopped brewing\");\n    }\n  }\n}\n```\n\nTo start turning this new `SetCoffeeMachineBrewing` method into an RPC that server can call, create an instance of the class and pass it to the client's registry like so:\n\n```typescript\n// remote-control.ts\n\nconst registry = new Registry(\n  new RemoteControl(), // This line is new\n  new CoffeeMachine(),\n\n  {\n    onClientConnect: () =\u003e {\n      clients++;\n\n      console.log(clients, \"coffee machines connected\");\n    },\n    onClientDisconnect: () =\u003e {\n      clients--;\n\n      console.log(clients, \"coffee machines connected\");\n    },\n  }\n);\n```\n\nThe remote control/client now exposes the `SetCoffeeMachineBrewing` RPC, and we can start enabling the coffee machine/server to call it by defining a basic class with a method that mirrors the RPC, just like we did before on the remote control for `BrewCoffee`:\n\n```typescript\n// coffee-machine.ts\n\nclass RemoteControl {\n  async SetCoffeeMachineBrewing(ctx: IRemoteContext, brewing: boolean) {}\n}\n```\n\nIn order to make the `SetCoffeeMachineBrewing` placeholder method do RPC calls, create an instance of the class and pass it to the server's registry like so:\n\n```typescript\n// coffee-machine.ts\n\nconst registry = new Registry(\n  service,\n  new RemoteControl(), // This line is new\n\n  {\n    onClientConnect: () =\u003e {\n      clients++;\n\n      console.log(clients, \"remote controls connected\");\n    },\n    onClientDisconnect: () =\u003e {\n      clients--;\n\n      console.log(clients, \"remote controls connected\");\n    },\n  }\n);\n```\n\nThe coffee machine/server and the remote control/client now both know of the new `SetCoffeeMachineBrewing` RPC, but the server doesn't call it yet. To fix this, we can call this RPC transparently from the coffee machine by accessing the connected remote control(s) with `registry.forRemotes` just like we did before in the remote control, and we can handle errors with `try catch` just like if we were making a local function call. We'll also use the first argument to the RPC, `ILocalContext`, to get the ID of the remote control/client that is calling `BrewCoffee`, so that we don't call `SetCoffeeMachineBrewing` on the remote control/client that is calling `BrewCoffee` itself:\n\n```typescript\n// coffee-machine.ts\n\nclass CoffeeMachine {\n  public forRemotes?: (\n    cb: (remoteID: string, remote: RemoteControl) =\u003e Promise\u003cvoid\u003e\n  ) =\u003e Promise\u003cvoid\u003e;\n\n  // ...\n\n  async BrewCoffee(\n    ctx: ILocalContext,\n    variant: string,\n    size: number\n  ): Promise\u003cnumber\u003e {\n    // Get the ID of the remote control that's calling `BrewCoffee`\n    const { remoteID: targetID } = ctx;\n\n    try {\n      // Notify connected remote controls that coffee is brewing\n      await this.forRemotes?.(async (remoteID, remote) =\u003e {\n        // Don't call `SetCoffeeMachineBrewing` if it's the remote control that's calling `BrewCoffee`\n        if (remoteID === targetID) {\n          return;\n        }\n\n        await remote.SetCoffeeMachineBrewing(undefined, true);\n      });\n\n      if (!this.supportedVariants.includes(variant)) {\n        throw new Error(\"unsupported variant\");\n      }\n\n      if (this.#waterLevel - size \u003c 0) {\n        throw new Error(\"not enough water\");\n      }\n\n      console.log(\"Brewing coffee variant\", variant, \"in size\", size, \"ml\");\n\n      await new Promise((r) =\u003e {\n        setTimeout(r, 5000);\n      });\n    } finally {\n      // Notify connected remote controls that coffee is no longer brewing\n      await this.forRemotes?.(async (remoteID, remote) =\u003e {\n        // Don't call `SetCoffeeMachineBrewing` if it's the remote control that's calling `BrewCoffee`\n        if (remoteID === targetID) {\n          return;\n        }\n\n        await remote.SetCoffeeMachineBrewing(undefined, false);\n      });\n    }\n\n    this.#waterLevel -= size;\n\n    return this.#waterLevel;\n  }\n}\n```\n\nNote that we've added the `forRemotes` field to the coffee machine/server; we can get the implementation for it from the registry like so:\n\n```typescript\n// coffee-machine.ts\n\nconst service = // ...\n\nconst registry = // ...\n\nservice.forRemotes = registry.forRemotes;\n```\n\nNow that we've added support for this RPC to the coffee machine/server, we can restart it like so:\n\n```shell\n$ npx tsx coffee-machine.ts\n```\n\nTo test if it works, connect two remote controls/clients to it like so:\n\n```shell\n$ npx tsx remote-control.ts\n# In another terminal\n$ npx tsx remote-control.ts\n```\n\nYou can now request the coffee machine to brew a coffee on either of the remote controls by pressing a number and \u003ckbd\u003eENTER\u003c/kbd\u003e. Once the RPC has been called, the coffee machine should print something like the following again:\n\n```plaintext\nBrewing coffee variant latte in size 100 ml\n```\n\nAnd after the coffee has been brewed, the remote control that you've chosen to brew the coffee with should once again return the remaining water level like so:\n\n```plaintext\nRemaining water: 900 ml\n```\n\nThe other connected remote controls will be notified that the coffee machine is brewing, and then once it has finished brewing:\n\n```plaintext\nCoffee machine is now brewing\nCoffee machine has stopped brewing\n```\n\n**Enjoy your distributed coffee machine!** You've successfully called an RPC provided by a client from the server to implement multicast notifications, something that usually is quite complex to do with RPC systems.\n\n\u003c/details\u003e\n\n#### 6. Passing Closures to RPCs\n\nSo far, when the remote control/client calls the `BrewCoffee` RPC, there is no way of knowing the incremental progress of the brew other than waiting for `BrewCoffee` to return the new water level. In order to know of the progress of the coffee machine as it is brewing, we can make use of the closure/callback support in panrpc, which allows us to pass a function to an RPC call, just like you could do locally.\n\n\u003cdetails\u003e\n  \u003csummary\u003eExpand section\u003c/summary\u003e\n\nFirst, we'll add a `onProgress` callback to the coffee machine's `BrewCoffee` implementation and decorate it with [panrpc's @remoteClosure decorator](https://pojntfx.github.io/panrpc/functions/remoteClosure.html), which we then call incrementally during the brewing process:\n\n```typescript\n// coffee-machine.ts\n\nimport { remoteClosure } from \"@pojntfx/panrpc\";\n\nclass CoffeeMachine {\n  // ...\n\n  async BrewCoffee(\n    ctx: ILocalContext,\n    variant: string,\n    size: number,\n    @remoteClosure\n    onProgress: (ctx: IRemoteContext, percentage: number) =\u003e Promise\u003cvoid\u003e // This is new\n  ): Promise\u003cnumber\u003e {\n    // ...\n\n    try {\n      // ...\n\n      // Report 0% brewing process\n      await onProgress(undefined, 0);\n\n      // Report 25% brewing process\n      await new Promise((r) =\u003e {\n        setTimeout(r, 500);\n      });\n      await onProgress(undefined, 25);\n\n      // Report 50% brewing process\n      await new Promise((r) =\u003e {\n        setTimeout(r, 500);\n      });\n      await onProgress(undefined, 50);\n\n      // Report 75% brewing process\n      await new Promise((r) =\u003e {\n        setTimeout(r, 500);\n      });\n      await onProgress(undefined, 75);\n\n      // Report 100% brewing process\n      await new Promise((r) =\u003e {\n        setTimeout(r, 500);\n      });\n      await onProgress(undefined, 100);\n    }\n\n    // ..\n\n    return this.#waterLevel;\n  }\n}\n```\n\nIn the remote control, we'll also extend the class with the `BrewCoffee` placeholder method with this new RPC argument:\n\n```typescript\n// remote-control.ts\n\nclass CoffeeMachine {\n  async BrewCoffee(\n    ctx: IRemoteContext,\n    variant: string,\n    size: number,\n    onProgress: (ctx: ILocalContext, percentage: number) =\u003e Promise\u003cvoid\u003e // This is new\n  ): Promise\u003cnumber\u003e {\n    return 0;\n  }\n}\n```\n\nAnd finally, where we call the `BrewCoffee` RPC in the remote control/client, we can pass in the implementation of this closure:\n\n```typescript\n// remote-control.ts\n\n(async () =\u003e {\n  // ...\n  await registry.forRemotes(async (remoteID, remote) =\u003e {\n    switch (line) {\n      case \"1\":\n      case \"2\":\n        // ...\n        const res = await remote.BrewCoffee(\n          undefined,\n          \"latte\",\n          line === \"1\" ? 100 : 200,\n          async (ctx, percentage) =\u003e\n            console.log(`Brewing Cafè Latte ... ${percentage}% done`) // This is new\n        );\n\n      // ...\n\n      case \"3\":\n      case \"4\":\n        // ...\n        const res = await remote.BrewCoffee(\n          undefined,\n          \"americano\",\n          line === \"3\" ? 100 : 200,\n          async (ctx, percentage) =\u003e\n            console.log(`Brewing Americano ... ${percentage}% done`) // This is new\n        );\n\n      // ..\n    }\n  });\n})();\n```\n\nNow we can restart the coffee machine/server again like so:\n\n```shell\n$ npx tsx coffee-machine.ts\n```\n\nAnd connect the remote control/client to it again like so:\n\n```shell\n$ npx tsx remote-control.ts\n```\n\nYou can now request the coffee machine to brew a coffee by pressing a number and \u003ckbd\u003eENTER\u003c/kbd\u003e. Once the RPC has been called, the coffee machine should print something like the following again:\n\n```plaintext\nBrewing coffee variant latte in size 100 ml\n```\n\nAnd the remote control will print the progress as reported by the coffee machine to the terminal, before once again returning the remaining water level like so:\n\n```plaintext\nBrewing Cafè Latte ... 0% done\nBrewing Cafè Latte ... 25% done\nBrewing Cafè Latte ... 50% done\nBrewing Cafè Latte ... 75% done\nBrewing Cafè Latte ... 100% done\nRemaining water: 900 ml\n```\n\n**Enjoy your live coffee brewing progress!** You've successfully implemented incremental coffee brewing progress reports by using panrpc's closure support, something that is usually quite tricky to do with RPC frameworks.\n\n\u003c/details\u003e\n\n#### 7. Nesting RPCs\n\nSo far, we've added RPCs directly to our coffee machine/server and remote control/client. While this approach is simple, it makes future extensions difficult. If we want to add more features, we would need to modify the coffee machine/server and remote control/client directly by adding new RPC methods, which can be hard to do in a type-safe way. Additionally, having only one level of RPCs makes large APIs hard to understand and organize as the number of RPCs increases.\n\nIn order to work around this, panrpc supports nesting RPCs in both clients and servers. This allows you to simplify top-level RPC calls; for example, instead of a single RPC like `GetConnectedChatUsers()`, you can use categorized, nested calls such as `Chat.Users.GetConnected()`.\n\n\u003cdetails\u003e\n  \u003csummary\u003eExpand section\u003c/summary\u003e\n\nTo define a nested RPC, simply add another class as a public instance property to your existing coffee machine/server or remote control/client. In this new class, you can define RPCs as methods, just like with top-level RPCs. To call them, follow the same concept: Define placeholder methods in the new class and add it as a public instance property to your coffee machine/server or remote control/client.\n\nIn this example, we'll add a tea brewer extension to our coffee machine/server and remote control/client. This extension will allow us to list available tea variants by calling the `Extension.GetVariants` RPC. For brevity, we won't implement all the tea brewing functions. To add the tea brewer extension to the coffee machine/server, first create a new `TeaBrewer` class, similar to the `CoffeeMachine` class. Then, add the `GetVariants` method to this class and instantiate it with the supported variants:\n\n```typescript\n// coffee-machine.ts\n\nclass TeaBrewer {\n  #supportedVariants: string[];\n\n  constructor(supportedVariants: string[]) {\n    this.#supportedVariants = supportedVariants;\n\n    this.GetVariants = this.GetVariants.bind(this);\n  }\n\n  async GetVariants(ctx: ILocalContext): Promise\u003cstring[]\u003e {\n    return this.#supportedVariants;\n  }\n}\n```\n\nTo add these nested RPCs to the main `CoffeeMachine` class, we'll include them as a public property in the class’s constructor. To keep the coffee machine flexible and avoid depending on the `TeaBrewer` extension, we'll name the property `Extension` and make it generic in `CoffeeMachine`. This way, we can easily replace the tea brewer with another extension in the future, such as a full-featured tea brewer:\n\n```typescript\n// coffee-machine.ts\n\nclass CoffeeMachine\u003cE\u003e {\n  // ...\n  constructor(\n    public Extension: E,\n    supportedVariants: string[],\n    waterLevel: number\n  ) {\n    this.#supportedVariants = supportedVariants;\n    this.#waterLevel = waterLevel;\n\n    this.BrewCoffee = this.BrewCoffee.bind(this);\n  }\n  // ...\n}\n```\n\nFinally, for the coffee machine/server, create an instance of our extension and pass it to the `CoffeeMachine` when initializing it. This is also where you provide the list of supported tea variants:\n\n```typescript\n// coffee-machine.ts\n\n// ...\nconst service = new CoffeeMachine(\n  new TeaBrewer([\"darjeeling\", \"chai\", \"earlgrey\"]),\n  [\"latte\", \"americano\"],\n  1000\n);\n// ...\n```\n\nFor the remote control/client, it's a very similar process. First, we define the new `TeaBrewer` class with the `GetVariants` placeholder method:\n\n```typescript\n// remote-control.ts\n\nclass TeaBrewer {\n  async GetVariants(ctx: IRemoteContext): Promise\u003cstring[]\u003e {\n    return [];\n  }\n}\n```\n\nThen we'll add the nested RPCs to the main `CoffeeMachine` class as a public instance property, and we'll use generics again to keep everything extensible:\n\n```typescript\n// remote-control.ts\n\nclass CoffeeMachine\u003cE\u003e {\n  constructor(public Extension: E) {}\n\n  // ...\n}\n```\n\nAfter this, we need to create an instance of our extension and pass it to `CoffeeMachine` when we create it:\n\n```typescript\n// remote-control.ts\n\nconst registry = new Registry(\n  new RemoteControl(),\n  new CoffeeMachine(new TeaBrewer())\n\n  // ...\n);\n```\n\nAnd finally, we add another switch case to the remote control/client so that we can call `Extension.GetVariants`:\n\n```typescript\n// remote-control.ts\n\n(async () =\u003e {\n  console.log(`Enter one of the following numbers followed by \u003cENTER\u003e to brew a coffee:\n\n- 1: Brew small Cafè Latte\n- 2: Brew large Cafè Latte\n\n- 3: Brew small Americano\n- 4: Brew large Americano\n\nOr enter 5 to list available tea variants.`);\n\n  // ...\n\n  await registry.forRemotes(async (remoteID, remote) =\u003e {\n    switch (line) {\n      // ..\n      case \"5\":\n        try {\n          const res = await remote.Extension.GetVariants(undefined);\n\n          console.log(\"Available tea variants:\", res);\n        } catch (e) {\n          console.error(`Couldn't list available tea variants: ${e}`);\n        }\n\n        break;\n\n      default:\n      // ..\n    }\n  });\n})();\n```\n\nNow we can restart the coffee machine/server again like so:\n\n```shell\n$ npx tsx coffee-machine.ts\n```\n\nAnd connect the remote control/client to it again like so:\n\n```shell\n$ npx tsx remote-control.ts\n```\n\nYou can now request the coffee machine to list the available tea variants by pressing `5` and \u003ckbd\u003eENTER\u003c/kbd\u003e. Once the RPC has been called, the remote control should print something like the following:\n\n```plaintext\nAvailable tea variants: [ \"darjeeling\", \"chai\", \"earlgrey\" ]\n```\n\n**🚀 That's it!** You've successfully built a virtual coffee machine with support for brewing coffee, notifications when coffee is being brewed, and incremental coffee brewing progress reports. You've also made it easily extensible by using nested RPCs. We can't wait to see what you're going to build next with panrpc! Be sure to take a look at the [reference](#reference) and [examples](#examples) for more information, or check out the complete sources for the [coffee machine server](./ts/bin/panrpc-example-websocket-coffee-server-cli.ts) and [coffee machine client/remote control](./ts/bin/panrpc-example-websocket-coffee-client-cli.ts) for a recap.\n\n\u003c/details\u003e\n\n## Reference\n\n### Library API\n\n- [![Go Reference](https://pkg.go.dev/badge/github.com/pojntfx/panrpc/go.svg)](https://pkg.go.dev/github.com/pojntfx/panrpc/go)\n- [![TypeScript docs](https://img.shields.io/badge/TypeScript%20-docs-blue.svg)](https://pojntfx.github.io/panrpc)\n\n### Examples\n\nTo make getting started with panrpc easier, take a look at the following examples:\n\n- **Transports**\n  - **TCP (Stream-Oriented API)**\n    - [\u003cimg alt=\"Go\" src=\"https://cdn.simpleicons.org/go\" style=\"vertical-align: middle;\" height=\"20\" width=\"20\" /\u003e TCP Server CLI Example](./go/cmd/panrpc-example-tcp-server-cli/main.go)\n    - [\u003cimg alt=\"Go\" src=\"https://cdn.simpleicons.org/go\" style=\"vertical-align: middle;\" height=\"20\" width=\"20\" /\u003e TCP Server CLI Example (with graceful shutdown)](./go/cmd/panrpc-example-tcp-server-graceful-cli/main.go)\n    - [\u003cimg alt=\"Go\" src=\"https://cdn.simpleicons.org/go\" style=\"vertical-align: middle;\" height=\"20\" width=\"20\" /\u003e TCP Client CLI Example](./go/cmd/panrpc-example-tcp-client-cli/main.go)\n    - [\u003cimg alt=\"Go\" src=\"https://cdn.simpleicons.org/go\" style=\"vertical-align: middle;\" height=\"20\" width=\"20\" /\u003e TCP Client CLI Example (with graceful shutdown)](./go/cmd/panrpc-example-tcp-client-graceful-cli/main.go)\n    - [\u003cimg alt=\"typescript\" src=\"https://cdn.simpleicons.org/typescript\" style=\"vertical-align: middle;\" height=\"20\" width=\"20\" /\u003e TCP Server CLI Example](./ts/bin/panrpc-example-tcp-server-cli.ts)\n    - [\u003cimg alt=\"typescript\" src=\"https://cdn.simpleicons.org/typescript\" style=\"vertical-align: middle;\" height=\"20\" width=\"20\" /\u003e TCP Client CLI Example](./ts/bin/panrpc-example-tcp-client-cli.ts)\n  - **UNIX Socket (Stream-Oriented API)**\n    - [\u003cimg alt=\"Go\" src=\"https://cdn.simpleicons.org/go\" style=\"vertical-align: middle;\" height=\"20\" width=\"20\" /\u003e UNIX Socket Server CLI Example](./go/cmd/panrpc-example-unix-server-cli/main.go)\n    - [\u003cimg alt=\"Go\" src=\"https://cdn.simpleicons.org/go\" style=\"vertical-align: middle;\" height=\"20\" width=\"20\" /\u003e UNIX Socket Client CLI Example](./go/cmd/panrpc-example-unix-client-cli/main.go)\n    - [\u003cimg alt=\"typescript\" src=\"https://cdn.simpleicons.org/typescript\" style=\"vertical-align: middle;\" height=\"20\" width=\"20\" /\u003e UNIX Socket Server CLI Example](./ts/bin/panrpc-example-unix-server-cli.ts)\n    - [\u003cimg alt=\"typescript\" src=\"https://cdn.simpleicons.org/typescript\" style=\"vertical-align: middle;\" height=\"20\" width=\"20\" /\u003e UNIX Socket Client CLI Example](./ts/bin/panrpc-example-unix-client-cli.ts)\n  - **`stdin/stdout` Pipe (Stream-Oriented API)**\n    - [\u003cimg alt=\"Go\" src=\"https://cdn.simpleicons.org/go\" style=\"vertical-align: middle;\" height=\"20\" width=\"20\" /\u003e `stdin/stdout` Pipe Socket Server CLI Example](./go/cmd/panrpc-example-pipe-server-cli/main.go)\n    - [\u003cimg alt=\"Go\" src=\"https://cdn.simpleicons.org/go\" style=\"vertical-align: middle;\" height=\"20\" width=\"20\" /\u003e `stdin/stdout` Pipe Socket Client CLI Example](./go/cmd/panrpc-example-pipe-client-cli/main.go)\n    - [\u003cimg alt=\"typescript\" src=\"https://cdn.simpleicons.org/typescript\" style=\"vertical-align: middle;\" height=\"20\" width=\"20\" /\u003e `stdin/stdout` Pipe Server CLI Example](./ts/bin/panrpc-example-pipe-server-cli.ts)\n    - [\u003cimg alt=\"typescript\" src=\"https://cdn.simpleicons.org/typescript\" style=\"vertical-align: middle;\" height=\"20\" width=\"20\" /\u003e `stdin/stdout` Pipe Client CLI Example](./ts/bin/panrpc-example-pipe-client-cli.ts)\n  - **WebSocket (Stream-Oriented API)**\n    - \u003cimg alt=\"Go\" src=\"https://cdn.simpleicons.org/go\" style=\"vertical-align: middle;\" height=\"20\" width=\"20\" /\u003e [WebSocket Server CLI Example](./go/cmd/panrpc-example-websocket-server-cli/main.go)\n    - [\u003cimg alt=\"Go\" src=\"https://cdn.simpleicons.org/go\" style=\"vertical-align: middle;\" height=\"20\" width=\"20\" /\u003e WebSocket Client CLI Example](./go/cmd/panrpc-example-websocket-client-cli/main.go)\n    - [\u003cimg alt=\"typescript\" src=\"https://cdn.simpleicons.org/typescript\" style=\"vertical-align: middle;\" height=\"20\" width=\"20\" /\u003e WebSocket Server CLI Example](./ts/bin/panrpc-example-websocket-server-cli.ts)\n    - [\u003cimg alt=\"typescript\" src=\"https://cdn.simpleicons.org/typescript\" style=\"vertical-align: middle;\" height=\"20\" width=\"20\" /\u003e WebSocket Client CLI Example](./ts/bin/panrpc-example-websocket-client-cli.ts)\n    - [\u003cimg alt=\"typescript\" src=\"https://cdn.simpleicons.org/typescript\" style=\"vertical-align: middle;\" height=\"20\" width=\"20\" /\u003e WebSocket Client Web Example](./ts/bin/panrpc-example-websocket-client-web)\n  - **WebRTC/weron (Stream-Oriented API)**\n    - [\u003cimg alt=\"Go\" src=\"https://cdn.simpleicons.org/go\" style=\"vertical-align: middle;\" height=\"20\" width=\"20\" /\u003e WebRTC/weron Peer CLI Example](./go/cmd/panrpc-example-webrtc-peer-cli/main.go)\n    - [\u003cimg alt=\"typescript\" src=\"https://cdn.simpleicons.org/typescript\" style=\"vertical-align: middle;\" height=\"20\" width=\"20\" /\u003e WebRTC/weron Peer CLI Example](./ts/bin/panrpc-example-webrtc-peer-cli.ts)\n  - **Valkey/Redis (Message-Oriented API)**\n    - [\u003cimg alt=\"Go\" src=\"https://cdn.simpleicons.org/go\" style=\"vertical-align: middle;\" height=\"20\" width=\"20\" /\u003e Valkey/Redis Server CLI Example](./go/cmd/panrpc-example-valkey-server-cli/main.go)\n    - [\u003cimg alt=\"Go\" src=\"https://cdn.simpleicons.org/go\" style=\"vertical-align: middle;\" height=\"20\" width=\"20\" /\u003e Valkey/Redis Client CLI Example](./go/cmd/panrpc-example-valkey-client-cli/main.go)\n    - [\u003cimg alt=\"typescript\" src=\"https://cdn.simpleicons.org/typescript\" style=\"vertical-align: middle;\" height=\"20\" width=\"20\" /\u003e Valkey/Redis Server CLI Example](./ts/bin/panrpc-example-valkey-server-cli.ts)\n    - [\u003cimg alt=\"typescript\" src=\"https://cdn.simpleicons.org/typescript\" style=\"vertical-align: middle;\" height=\"20\" width=\"20\" /\u003e Valkey/Redis Client CLI Example](./ts/bin/panrpc-example-valkey-client-cli.ts)\n- **Callbacks**\n  - [\u003cimg alt=\"Go\" src=\"https://cdn.simpleicons.org/go\" style=\"vertical-align: middle;\" height=\"20\" width=\"20\" /\u003e Callbacks Demo Server CLI Example](./go/cmd/panrpc-example-tcp-callbacks-callee-cli/main.go)\n  - [\u003cimg alt=\"Go\" src=\"https://cdn.simpleicons.org/go\" style=\"vertical-align: middle;\" height=\"20\" width=\"20\" /\u003e Callbacks Demo Client CLI Example](./go/cmd/panrpc-example-tcp-callbacks-caller-cli/main.go)\n  - [\u003cimg alt=\"typescript\" src=\"https://cdn.simpleicons.org/typescript\" style=\"vertical-align: middle;\" height=\"20\" width=\"20\" /\u003e Callbacks Demo Server CLI Example](./ts/bin/panrpc-example-tcp-callbacks-callee-cli.ts)\n  - [\u003cimg alt=\"typescript\" src=\"https://cdn.simpleicons.org/typescript\" style=\"vertical-align: middle;\" height=\"20\" width=\"20\" /\u003e Callbacks Demo Client CLI Example](./ts/bin/panrpc-example-tcp-callbacks-caller-cli.ts)\n- **Closures**\n  - [\u003cimg alt=\"Go\" src=\"https://cdn.simpleicons.org/go\" style=\"vertical-align: middle;\" height=\"20\" width=\"20\" /\u003e Closures Demo Server CLI Example](./go/cmd/panrpc-example-tcp-closures-callee-cli/main.go)\n  - [\u003cimg alt=\"Go\" src=\"https://cdn.simpleicons.org/go\" style=\"vertical-align: middle;\" height=\"20\" width=\"20\" /\u003e Closures Demo Client CLI Example](./go/cmd/panrpc-example-tcp-closures-caller-cli/main.go)\n  - [\u003cimg alt=\"typescript\" src=\"https://cdn.simpleicons.org/typescript\" style=\"vertical-align: middle;\" height=\"20\" width=\"20\" /\u003e Closures Demo Server CLI Example](./ts/bin/panrpc-example-tcp-closures-callee-cli.ts)\n  - [\u003cimg alt=\"typescript\" src=\"https://cdn.simpleicons.org/typescript\" style=\"vertical-align: middle;\" height=\"20\" width=\"20\" /\u003e Closures Demo Client CLI Example](./ts/bin/panrpc-example-tcp-closures-caller-cli.ts)\n- **Nested RPCs**\n  - [\u003cimg alt=\"Go\" src=\"https://cdn.simpleicons.org/go\" style=\"vertical-align: middle;\" height=\"20\" width=\"20\" /\u003e Nested Server CLI Example](./go/cmd/panrpc-example-tcp-nested-server-cli/main.go)\n  - [\u003cimg alt=\"Go\" src=\"https://cdn.simpleicons.org/go\" style=\"vertical-align: middle;\" height=\"20\" width=\"20\" /\u003e Nested Client CLI Example](./go/cmd/panrpc-example-tcp-nested-client-cli/main.go)\n  - [\u003cimg alt=\"typescript\" src=\"https://cdn.simpleicons.org/typescript\" style=\"vertical-align: middle;\" height=\"20\" width=\"20\" /\u003e Nested Server CLI Example](./ts/bin/panrpc-example-tcp-nested-server-cli.ts)\n  - [\u003cimg alt=\"typescript\" src=\"https://cdn.simpleicons.org/typescript\" style=\"vertical-align: middle;\" height=\"20\" width=\"20\" /\u003e Nested Client CLI Example](./ts/bin/panrpc-example-tcp-nested-client-cli.ts)\n- **Benchmarks**\n  - [\u003cimg alt=\"Go\" src=\"https://cdn.simpleicons.org/go\" style=\"vertical-align: middle;\" height=\"20\" width=\"20\" /\u003e Requests/Second Benchmark Server CLI Example](./go/cmd/panrpc-example-tcp-rps-server-cli/main.go)\n  - [\u003cimg alt=\"Go\" src=\"https://cdn.simpleicons.org/go\" style=\"vertical-align: middle;\" height=\"20\" width=\"20\" /\u003e Requests/Second Benchmark Client CLI Example](./go/cmd/panrpc-example-tcp-rps-client-cli/main.go)\n  - [\u003cimg alt=\"typescript\" src=\"https://cdn.simpleicons.org/typescript\" style=\"vertical-align: middle;\" height=\"20\" width=\"20\" /\u003e Requests/Second Benchmark Server CLI Example](./ts/bin/panrpc-example-tcp-rps-server-cli.ts)\n  - [\u003cimg alt=\"typescript\" src=\"https://cdn.simpleicons.org/typescript\" style=\"vertical-align: middle;\" height=\"20\" width=\"20\" /\u003e Requests/Second Benchmark Client CLI Example](./ts/bin/panrpc-example-tcp-rps-client-cli.ts)\n  - [\u003cimg alt=\"Go\" src=\"https://cdn.simpleicons.org/go\" style=\"vertical-align: middle;\" height=\"20\" width=\"20\" /\u003e Throughput Benchmark Server CLI Example](./go/cmd/panrpc-example-tcp-throughput-server-cli/main.go)\n  - [\u003cimg alt=\"Go\" src=\"https://cdn.simpleicons.org/go\" style=\"vertical-align: middle;\" height=\"20\" width=\"20\" /\u003e Throughput Benchmark Client CLI Example](./go/cmd/panrpc-example-tcp-throughput-client-cli/main.go)\n  - [\u003cimg alt=\"typescript\" src=\"https://cdn.simpleicons.org/typescript\" style=\"vertical-align: middle;\" height=\"20\" width=\"20\" /\u003e Throughput Benchmark Server CLI Example](./ts/bin/panrpc-example-tcp-throughput-server-cli.ts)\n  - [\u003cimg alt=\"typescript\" src=\"https://cdn.simpleicons.org/typescript\" style=\"vertical-align: middle;\" height=\"20\" width=\"20\" /\u003e Throughput Benchmark Client CLI Example](./ts/bin/panrpc-example-tcp-throughput-client-cli.ts)\n\n### Benchmarks\n\nAll benchmarks were conducted on a test machine with the following specifications:\n\n| Property     | Value                                   |\n| ------------ | --------------------------------------- |\n| Device Model | Dell XPS 9320                           |\n| OS           | Fedora release 38 (Thirty Eight) x86_64 |\n| Kernel       | 6.3.11-200.fc38.x86_64                  |\n| CPU          | 12th Gen Intel i7-1280P (20) @ 4.700GHz |\n| Memory       | 31687MiB LPDDR5, 6400 MT/s              |\n\nTo reproduce the tests, see the [benchmark source code](#examples) and the [visualization source code](./docs/).\n\n#### Requests/Second\n\n\u003e This is measured by calling RPCs with the different data types as the arguments.\n\n\u003cimg src=\"./docs/rps.png\" alt=\"Bar chart of the requests/second benchmark results for JSON and CBOR\" width=\"550px\"\u003e\n\n| Data Type  | JSON (go) | CBOR (go) | JSON (typescript) | CBOR (typescript) |\n| :--------- | --------: | --------: | ----------------: | ----------------: |\n| array      |     75500 |     99683 |             57373 |             62848 |\n| bool       |     79662 |    106226 |             57499 |             63324 |\n| byte       |     81438 |    105916 |             57480 |             60169 |\n| complex128 |       nan |       nan |             58849 |             59693 |\n| complex64  |       nan |       nan |             58375 |             63018 |\n| float32    |     79878 |    106359 |             54034 |             62068 |\n| float64    |     78724 |    101498 |             55181 |             61987 |\n| int        |     93569 |    119268 |             52115 |             59269 |\n| int16      |     76995 |    104569 |             56596 |             62165 |\n| int32      |     80425 |    106986 |             53847 |             63676 |\n| int64      |     81276 |    101144 |             58126 |             64622 |\n| int8       |     85734 |    113260 |             54081 |             60756 |\n| rune       |     84113 |    109719 |             53753 |             61153 |\n| slice      |     77975 |    101126 |             56404 |             62278 |\n| string     |     77252 |    106265 |             57876 |             60453 |\n| struct     |     77699 |    104968 |             57876 |             61498 |\n| uint       |     81361 |    103698 |             58455 |             61729 |\n| uint16     |     80990 |    106615 |             57004 |             62429 |\n| uint32     |     80319 |    103672 |             55668 |             63651 |\n| uint64     |     82412 |    107139 |             53627 |             63818 |\n| uint8      |     82127 |    106076 |             59698 |             59955 |\n| uintptr    |       nan |       nan |             53214 |             64170 |\n\n#### Throughput\n\n\u003e This is measured by calling an RPC with `[]byte` as the argument.\n\n\u003cimg src=\"./docs/throughput.png\" alt=\"Bar chart of the throughput benchmark results for JSON and CBOR\" width=\"550px\"\u003e\n\n| Serializer        | Average Throughput |\n| ----------------- | ------------------ |\n| CBOR (go)         | 1389 MB/s          |\n| JSON (go)         | 105 MB/s           |\n| CBOR (typescript) | 24 MB/s            |\n| JSON (typescript) | 12 MB/s            |\n\n### Protocol\n\nThe protocol used by panrpc is simple and independent of transport and serialization layer; in the following examples, we'll use JSON.\n\nA function call to e.g. the `Println` function from above looks like this:\n\n```json\n{\n  \"request\": {\n    \"call\": \"b3332cf0-4e50-4684-a909-05772e14595e\",\n    \"function\": \"Println\",\n    \"args\": [\"Hello, world!\"]\n  },\n  \"response\": null\n}\n```\n\nThe request/response wrapper specifies whether the message is a function call (`request`) or return (`response`). `call` is the ID of the function call, as generated by the client; `function` is the function name and `args` is an array of the function's arguments.\n\nA function return looks like this:\n\n```json\n{\n  \"request\": null,\n  \"response\": {\n    \"call\": \"b3332cf0-4e50-4684-a909-05772e14595e\",\n    \"value\": null,\n    \"err\": \"\"\n  }\n}\n```\n\nHere, `response` specifies that the message is a function return. `call` is the ID of the function call from above, `value` is the function's return value, and the last element is the error message; `nil` errors are represented by the empty string.\n\nKeep in mind that panrpc is bidirectional, meaning that both the client and server can send and receive both types of messages to each other.\n\n### `purl` Command Line Arguments\n\n```shell\n$ purl --help\nLike cURL, but for panrpc: Command-line tool for interacting with panrpc servers\n\nUsage of purl:\n\tpurl [flags] \u003c(tcp|tls|unix|unixs|ws|wss|weron)://(host:port/function|path/function|password:key@community/channel[/remote]/function)\u003e \u003c[args...]\u003e\n\nExamples:\n\tpurl tcp://localhost:1337/Increment '[1]'\n\tpurl tls://localhost:443/Increment '[1]'\n\tpurl unix:///tmp/panrpc.sock/Increment '[1]'\n\tpurl unixs:///tmp/panrpc.sock/Increment '[1]'\n\tpurl ws://localhost:1337/Increment '[1]'\n\tpurl wss://localhost:443/Increment '[1]'\n\tpurl weron://examplepass:examplekey@examplecommunity/panrpc.example.webrtc/Increment '[1]'\n\nFlags:\n  -listen\n    \tWhether to connect to remotes by listening or dialing (ignored for weron://)\n  -serializer string\n    \tSerializer to use (json or cbor) (default \"json\")\n  -timeout duration\n    \tTime to wait for a response to a call (default 10s)\n  -tls-cert string\n    \tTLS certificate (only valid for tls://, unixs:// and wss://)\n  -tls-key string\n    \tTLS key (only valid for tls://, unixs:// and wss://)\n  -tls-verify\n    \tWhether to verify TLS peer certificates (only valid for tls://, unixs:// and wss://) (default true)\n  -verbose\n    \tWhether to enable verbose logging\n  -weron-force-relay\n    \tForce usage of TURN servers (only valid for weron://)\n  -weron-ice string\n    \tComma-separated list of STUN servers (in format stun:host:port) and TURN servers to use (in format username:credential@turn:host:port) (i.e. username:credential@turn:global.turn.twilio.com:3478?transport=tcp) (only valid for weron://) (default \"stun:stun.l.google.com:19302\")\n  -weron-signaler string\n    \tSignaler address (only valid for weron://) (default \"wss://weron.up.railway.app/\")\n```\n\n## Acknowledgements\n\n- [zserge/lorca](https://github.com/zserge/lorca) inspired the API design.\n\n## Contributing\n\nTo contribute, please use the [GitHub flow](https://guides.github.com/introduction/flow/) and follow our [Code of Conduct](./CODE_OF_CONDUCT.md).\n\nTo build and start a development version of panrpc locally, run the following:\n\n```shell\n$ git clone https://github.com/pojntfx/panrpc.git\n\n# For Go\n$ cd panrpc/go\n$ go run ./cmd/panrpc-example-tcp-server-cli/ # Starts the Go TCP example server CLI\n# In another terminal\n$ go run ./cmd/panrpc-example-tcp-client-cli/ # Starts the Go TCP example client CLI\n\n# For TypeScript\n$ cd panrpc/ts\n$ npm install\n$ npx tsx ./bin/panrpc-example-tcp-server-cli.ts # Starts the TypeScript TCP example server CLI\n# In another terminal\n$ npx tsx ./bin/panrpc-example-tcp-client-cli.ts # Starts the TypeScript TCP example client CLI\n```\n\nHave any questions or need help? Chat with us [on Matrix](https://matrix.to/#/#panrpc:matrix.org?via=matrix.org)!\n\n## License\n\npanrpc (c) 2024 Felicitas Pojtinger and contributors\n\nSPDX-License-Identifier: Apache-2.0\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpojntfx%2Fpanrpc","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpojntfx%2Fpanrpc","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpojntfx%2Fpanrpc/lists"}