Skip to content

Design

The design of kkRPC is inspired by JSON-RPC 2.0 specification and Comlink.

I borrowed the idea of using proxy to make the API look like local calls from Comlink. Comlink is designed for iframe and web worker communication. I was using it in my project Kunkun and needed to extend it to support stdio communication, so I built comlink-stdio.

Later I encountered some problems with comlink in iframe and couldn’t find a good solution. The project seems not maintained. So I decided to build my own library by building on top of comlink-stdio to support more communication protocols (e.g. WebSocket, WebWorker, HTTP, etc.).

The HTTP adapter’s single endpoint design is inspired by GraphQL, which also has a single post endpoint for all requests. Actually, the overall design of kkRPC is very similar to GraphQL (i.e. sending query and response in JSON format over another protocol, to a single endpoint). kkRPC is much easier to use though. There is no need to define a schema file or to code generation.

The message structure is different from JSON-RPC 2.0, but similar in concept.

Each message can serve as a request, response or callback. method is used to locate the exposed API.

interface Message<T = any> {
id: string
method: string
args: T
type: "request" | "response" | "callback" // Add "callback" type
callbackIds?: string[] // Add callbackIds field
}

Since it’s not possible to transfer a callback function over any protocol, the channel keeps track of callbacks, send callback ids to the remote. When the remote “calls” the callback, it’s actually returning callback ids, then the local side will use the ids to find the callback function and call it.

Adapter

To make kkRPC work anywhere, IoInterface is introduced. It’s a common interface for any bidirectional communication channel.

interface IoInterface {
name: string
read(): Promise<Buffer | Uint8Array | string | null> // Reads input
write(data: string): Promise<void> // Writes output
}

name is only used for debugging.

Any environment that can establish a connection should be able to implement read and write function. read means reading data from the remote; write means writing data to the remote.

So as long as the environment can read and write, it can be used as a communication channel.

To adapt to a new environment, simply implement IoInterface and pass it to RPCChannel.

RPCChannel does all the underlying magic, including serialization/deserialization, request-response matching, callback managing, proxy generating, etc.

Extend to Other Languages

JS/TS has the advantage of dynamic typing and super free syntax which allows proxy, eventually allowing calling remote RPC methods like if the are local with TypeScript support.

kkRPC was created for TypeScript projects, it doesn’t have a schema like GraphQL or gRPC’s .proto file. This project will be so complicated if I want to do that, code generate for other languages will be a ton of work and I don’t want to do that.

Since the underlying protocol is quite simple (similar to JSON-RPC), it’s possible to extend to other languages. Just implement the same IO interface and channel in the target language, it’s not too hard.

The problem is, you can’t reuse the API type/interface from TypeScript, and there is most likely no proxy support (you will need to write the method names). In this case, I don’t think kkRPC is a good choice, you lose all the benefits of kkRPC (i.e. proxy, TypeScript, intellisense).

If you are sure you need other languages for features like callback, then you can implement your own channel and IO adapter.