Blog: [[Blogs/kkrpc|kkrpc]]
<div className="flex gap-2">
<a href="https://npmjs.com/package/kkrpc">
<img src="https://img.shields.io/npm/v/kkrpc" />
</a>
<a href="https://jsr.io/@kunkun/kkrpc">
<img src="https://jsr.io/badges/@kunkun/kkrpc" />
</a>
<a href="https://github.com/kunkunsh/kkrpc">
<img src="https://img.shields.io/github/last-commit/kunkunsh/kkrpc" />
</a>
</div>
> A TypeScript-first RPC library that enables seamless bi-directional communication between processes.
> Call remote functions as if they were local, with full TypeScript type safety and autocompletion support.
- [JSR Package](https://jsr.io/@kunkun/kkrpc)
- [NPM Package](https://www.npmjs.com/package/kkrpc)
- [Documentation by JSR](https://jsr.io/@kunkun/kkrpc/doc)
- [Typedoc Documentation](https://kunkunsh.github.io/kkrpc/)
<img src="https://imgur.com/vR3Lmv0.png" style={{ width: "30em" }} />
<img src="https://imgur.com/u728aVv.png" style={{ width: "30em" }} />

## Supported Environments
- stdio: RPC over stdio between any combinations of Node.js, Deno, Bun processes
- web: RPC over `postMessage` API and message channel between browser main thread and web workers, or main thread and iframe
- Web Worker API (web standard) is also supported in Deno and Bun, the main thread can call functions in worker and vice versa.
- http: RPC over HTTP like tRPC
- supports any HTTP server (e.g. hono, bun, nodejs http, express, fastify, deno, etc.)
- WebSocket: RPC over WebSocket
The core of **kkrpc** design is in `RPCChannel` and `IoInterface`.
- `RPCChannel` is the bidirectional RPC channel
- `LocalAPI` is the APIs to be exposed to the other side of the channel
- `RemoteAPI` is the APIs exposed by the other side of the channel, and callable on the local side
- `rpc.getAPI()` returns an object that is `RemoteAPI` typed, and is callable on the local side like a normal local function call.
- `IoInterface` is the interface for implementing the IO for different environments. The implementations are called adapters.
- For example, for a Node process to communicate with a Deno process, we need `NodeIo` and `DenoIo` adapters which implements `IoInterface`. They share the same stdio pipe (`stdin/stdout`).
- In web, we have `WorkerChildIO` and `WorkerParentIO` adapters for web worker, `IframeParentIO` and `IframeChildIO` adapters for iframe.
> In browser, import from `kkrpc/browser` instead of `kkrpc`, Deno adapter uses node:buffer which doesn't work in browser.
```ts
interface IoInterface {
name: string;
read(): Promise<Buffer | Uint8Array | string | null>; // Reads input
write(data: string): Promise<void>; // Writes output
}
class RPCChannel<
LocalAPI extends Record<string, any>,
RemoteAPI extends Record<string, any>,
Io extends IoInterface = IoInterface
> {}
```
## Examples
Below are simple examples.
### Stdio Example
```ts
import { NodeIo, RPCChannel } from "kkrpc";
import { apiMethods } from "./api.ts";
const stdio = new NodeIo(process.stdin, process.stdout);
const child = new RPCChannel(stdio, { expose: apiMethods });
```
```ts
import { spawn } from "child_process";
const worker = spawn("bun", ["scripts/node-api.ts"]);
const io = new NodeIo(worker.stdout, worker.stdin);
const parent = new RPCChannel<{}, API>(io);
const api = parent.getAPI();
expect(await api.add(1, 2)).toBe(3);
```
### Web Worker Example
```ts
import { RPCChannel, WorkerChildIO, type DestroyableIoInterface } from "kkrpc";
const worker = new Worker(
new URL("./scripts/worker.ts", import.meta.url).href,
{ type: "module" }
);
const io = new WorkerChildIO(worker);
const rpc = new RPCChannel<API, API, DestroyableIoInterface>(io, {
expose: apiMethods,
});
const api = rpc.getAPI();
expect(await api.add(1, 2)).toBe(3);
```
```ts
import { RPCChannel, WorkerParentIO, type DestroyableIoInterface } from "kkrpc";
const io: DestroyableIoInterface = new WorkerChildIO();
const rpc = new RPCChannel<API, API, DestroyableIoInterface>(io, {
expose: apiMethods,
});
const api = rpc.getAPI();
const sum = await api.add(1, 2);
expect(sum).toBe(3);
```
### HTTP Example
Codesandbox: https://codesandbox.io/p/live/4a349334-0b04-4352-89f9-cf1955553ae7
#### `api.ts`
Define API type and implementation.
```ts
export type API = {
echo: (message: string) => Promise<string>;
add: (a: number, b: number) => Promise<number>;
};
export const api: API = {
echo: (message) => {
return Promise.resolve(message);
},
add: (a, b) => {
return Promise.resolve(a + b);
},
};
```
#### `server.ts`
Server only requires a one-time setup, then it won't need to be touched again.
All the API implementation is in `api.ts`.
```ts
import { HTTPServerIO, RPCChannel } from "kkrpc";
import { api, type API } from "./api";
const serverIO = new HTTPServerIO();
const serverRPC = new RPCChannel<API, API>(serverIO, { expose: api });
const server = Bun.serve({
port: 3000,
async fetch(req) {
const url = new URL(req.url);
if (url.pathname === "/rpc") {
const res = await serverIO.handleRequest(await req.text());
return new Response(res, {
headers: { "Content-Type": "application/json" },
});
}
return new Response("Not found", { status: 404 });
},
});
console.log(`Start server on port: ${server.port}`);
```
#### `client.ts`
```ts
import { HTTPClientIO, RPCChannel } from "kkrpc";
import { api, type API } from "./api";
const clientIO = new HTTPClientIO({
url: "http://localhost:3000/rpc",
});
const clientRPC = new RPCChannel<{}, API>(clientIO, { expose: api });
const clientAPI = clientRPC.getAPI();
const echoResponse = await clientAPI.echo("hello");
console.log("echoResponse", echoResponse);
const sum = await clientAPI.add(2, 3);
console.log("Sum: ", sum);
```
### Chrome Extension Example
#### `background.ts`
```ts
import { ChromeBackgroundIO, RPCChannel } from "kkrpc";
import type { API } from "./api";
// Store RPC channels for each tab
const rpcChannels = new Map<number, RPCChannel<API, {}>>();
// Listen for tab connections
chrome.runtime.onConnect.addListener((port) => {
if (port.sender?.tab?.id) {
const tabId = port.sender.tab.id;
const io = new ChromeBackgroundIO(tabId);
const rpc = new RPCChannel(io, { expose: backgroundAPI });
rpcChannels.set(tabId, rpc);
port.onDisconnect.addListener(() => {
rpcChannels.delete(tabId);
});
}
});
```
#### `content.ts`
```ts
import { ChromeContentIO, RPCChannel } from "kkrpc";
import type { API } from "./api";
const io = new ChromeContentIO();
const rpc = new RPCChannel<API, API>(io, {
expose: {
updateUI: async (data) => {
document.body.innerHTML = data.message;
return true;
},
},
});
// Get API from background script
const api = rpc.getAPI();
const data = await api.getData();
console.log(data); // { message: "Hello from background!" }
```