https://github.com/kunkunsh/kkrpc
A TypeScript RPC protocol for multiple environments (iframe, web worker, stdio, http, WebSocket)
https://github.com/kunkunsh/kkrpc
http iframe stdio websocket webworker
Last synced: 19 days ago
JSON representation
A TypeScript RPC protocol for multiple environments (iframe, web worker, stdio, http, WebSocket)
- Host: GitHub
- URL: https://github.com/kunkunsh/kkrpc
- Owner: kunkunsh
- License: apache-2.0
- Created: 2024-11-17T01:06:17.000Z (7 months ago)
- Default Branch: main
- Last Pushed: 2025-03-30T15:06:57.000Z (2 months ago)
- Last Synced: 2025-05-12T22:47:56.899Z (19 days ago)
- Topics: http, iframe, stdio, websocket, webworker
- Language: TypeScript
- Homepage: https://docs.kkrpc.kunkun.sh/
- Size: 1.7 MB
- Stars: 7
- Watchers: 0
- Forks: 1
- Open Issues: 2
-
Metadata Files:
- Readme: README.md
- Funding: .github/FUNDING.yml
Awesome Lists containing this project
- awesome-tauri - kkrpc - Seamless RPC communication between a Tauri app and node/deno/bun processes, just like Electron. (Development / Integrations)
README
# kkrpc
> This project is created for building extension system for a Tauri app (https://github.com/kunkunsh/kunkun).
>
> It can potentially be used in other types of apps, so I open sourced it as a standalone package.[](https://www.npmjs.com/package/kkrpc)
[](https://jsr.io/@kunkun/kkrpc)
> 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/)[Excalidraw Diagrams](https://excalidraw.com/#json=xp6GbAJVAx3nU-h3PhaxW,oYBNvYmCRsQ2XR3MQo73Ug)
![]()
![]()
![]()
## 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 WebSocketThe 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 // Reads input
write(data: string): Promise // Writes output
}class RPCChannel<
LocalAPI extends Record,
RemoteAPI extends Record,
Io extends IoInterface = IoInterface
> {}
```## Serialization
kkrpc supports two serialization formats for message transmission:
- `json`: Standard JSON serialization
- `superjson`: Enhanced JSON serialization with support for more data types like Date, Map, Set, BigInt, and Uint8Array (default since v0.2.0)You can specify the serialization format when creating a new RPCChannel:
```ts
// Using default serialization (superjson)
const rpc = new RPCChannel(io, { expose: apiImplementation })// Explicitly using superjson serialization (recommended for clarity)
const rpc = new RPCChannel(io, {
expose: apiImplementation,
serialization: { version: "superjson" }
})// Using standard JSON serialization (for backward compatibility)
const rpc = new RPCChannel(io, {
expose: apiImplementation,
serialization: { version: "json" }
})
```For backward compatibility, the receiving side will automatically detect the serialization format so older clients can communicate with newer servers and vice versa.
## 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(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(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
add: (a: number, b: number) => Promise
}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(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>()// 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(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!" }
```### Tauri Example
Call functions in bun/node/deno processes from Tauri app with JS/TS.
It allows you to call any JS/TS code in Deno/Bun/Node processes from Tauri app, just like using Electron.
Seamless integration with Tauri's official shell plugin and [unlocked shellx plugin](https://github.com/HuakunShen/tauri-plugin-shellx).
```ts
import { RPCChannel, TauriShellStdio } from "kkrpc/browser"
import { Child, Command } from "@tauri-apps/plugin-shell"const localAPIImplementation = {
add: (a: number, b: number) => Promise.resolve(a + b)
}async function spawnCmd(runtime: "deno" | "bun" | "node") {
let cmd: Command
let process = Child | null = nullif (runtime === "deno") {
cmd = Command.create("deno", ["run", "-A", scriptPath])
process = await cmd.spawn()
} else if (runtime === "bun") {
cmd = Command.create("bun", [scriptPath])
process = await cmd.spawn()
} else if (runtime === "node") {
cmd = Command.create("node", [scriptPath])
process = await cmd.spawn()
} else {
throw new Error(`Invalid runtime: ${runtime}, pick either deno or bun`)
}// monitor stdout/stderr/close/error for debugging and error handling
cmd.stdout.on("data", (data) => {
console.log("stdout", data)
})
cmd.stderr.on("data", (data) => {
console.warn("stderr", data)
})
cmd.on("close", (code) => {
console.log("close", code)
})
cmd.on("error", (err) => {
console.error("error", err)
})const stdio = new TauriShellStdio(cmd.stdout, process)
const stdioRPC = new RPCChannel(stdio, {
expose: localAPIImplementation
})const api = stdioRPC.getAPI();
await api
.add(1, 2)
.then((result) => {
console.log("result", result)
})
.catch((err) => {
console.error(err)
})process?.kill()
}
```I provided a sample tauri app in `examples/tauri-demo`.
