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" }} /> ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1740657706728/f4ec6698-af0a-4793-ae55-7f3284e81d50.png) ## 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!" } ```