{"id":21701329,"url":"https://github.com/kunkunsh/kkrpc","last_synced_at":"2025-07-26T17:05:39.610Z","repository":{"id":263212526,"uuid":"889693591","full_name":"kunkunsh/kkrpc","owner":"kunkunsh","description":"A TypeScript RPC protocol for multiple environments (iframe, web worker, stdio, http, WebSocket)","archived":false,"fork":false,"pushed_at":"2025-03-30T15:06:57.000Z","size":1778,"stargazers_count":7,"open_issues_count":2,"forks_count":1,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-05-12T22:47:56.899Z","etag":null,"topics":["http","iframe","stdio","websocket","webworker"],"latest_commit_sha":null,"homepage":"https://docs.kkrpc.kunkun.sh/","language":"TypeScript","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/kunkunsh.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":null,"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},"funding":{"github":["HuakunShen"],"buy_me_a_coffee":"huakun"}},"created_at":"2024-11-17T01:06:17.000Z","updated_at":"2025-05-12T09:55:53.000Z","dependencies_parsed_at":null,"dependency_job_id":"dcc4d064-e9c5-4a52-b217-4364cdf48ca8","html_url":"https://github.com/kunkunsh/kkrpc","commit_stats":null,"previous_names":["kunkunsh/kkrpc"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kunkunsh%2Fkkrpc","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kunkunsh%2Fkkrpc/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kunkunsh%2Fkkrpc/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kunkunsh%2Fkkrpc/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/kunkunsh","download_url":"https://codeload.github.com/kunkunsh/kkrpc/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":253837387,"owners_count":21971981,"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":["http","iframe","stdio","websocket","webworker"],"created_at":"2024-11-25T20:19:03.379Z","updated_at":"2025-05-12T22:48:12.171Z","avatar_url":"https://github.com/kunkunsh.png","language":"TypeScript","readme":"# kkrpc\n\n\u003e This project is created for building extension system for a Tauri app (https://github.com/kunkunsh/kunkun).\n\u003e\n\u003e It can potentially be used in other types of apps, so I open sourced it as a standalone package.\n\n[![NPM Version](https://img.shields.io/npm/v/kkrpc)](https://www.npmjs.com/package/kkrpc)\n[![JSR Version](https://jsr.io/badges/@kunkun/kkrpc)](https://jsr.io/@kunkun/kkrpc)\n![GitHub last commit](https://img.shields.io/github/last-commit/kunkunsh/kkrpc)\n\n\u003e A TypeScript-first RPC library that enables seamless bi-directional communication between processes.\n\u003e Call remote functions as if they were local, with full TypeScript type safety and autocompletion support.\n\n- [JSR Package](https://jsr.io/@kunkun/kkrpc)\n- [NPM Package](https://www.npmjs.com/package/kkrpc)\n- [Documentation by JSR](https://jsr.io/@kunkun/kkrpc/doc)\n- [Typedoc Documentation](https://kunkunsh.github.io/kkrpc/)\n\n[Excalidraw Diagrams](https://excalidraw.com/#json=xp6GbAJVAx3nU-h3PhaxW,oYBNvYmCRsQ2XR3MQo73Ug)\n\n\u003cimg src=\"https://imgur.com/vR3Lmv0.png\" style=\"max-height: 200px;\"/\u003e\n\u003cimg src=\"https://i.imgur.com/zmOHNfu.png\" style=\"max-height: 250px;\"/\u003e\n\u003cimg src=\"https://imgur.com/u728aVv.png\" style=\"max-height: 400px;\"/\u003e\n\u003cimg src=\"https://i.imgur.com/Gu7jH1v.png\" style=\"max-height: 300px;\"/\u003e\n\n## Supported Environments\n\n- stdio: RPC over stdio between any combinations of Node.js, Deno, Bun processes\n- web: RPC over `postMessage` API and message channel between browser main thread and web workers, or main thread and iframe\n  - Web Worker API (web standard) is also supported in Deno and Bun, the main thread can call functions in worker and vice versa.\n- http: RPC over HTTP like tRPC\n  - supports any HTTP server (e.g. hono, bun, nodejs http, express, fastify, deno, etc.)\n- WebSocket: RPC over WebSocket\n\nThe core of **kkrpc** design is in `RPCChannel` and `IoInterface`.\n\n- `RPCChannel` is the bidirectional RPC channel\n- `LocalAPI` is the APIs to be exposed to the other side of the channel\n- `RemoteAPI` is the APIs exposed by the other side of the channel, and callable on the local side\n- `rpc.getAPI()` returns an object that is `RemoteAPI` typed, and is callable on the local side like a normal local function call.\n- `IoInterface` is the interface for implementing the IO for different environments. The implementations are called adapters.\n  - 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`).\n  - In web, we have `WorkerChildIO` and `WorkerParentIO` adapters for web worker, `IframeParentIO` and `IframeChildIO` adapters for iframe.\n\n\u003e In browser, import from `kkrpc/browser` instead of `kkrpc`, Deno adapter uses node:buffer which doesn't work in browser.\n\n```ts\ninterface IoInterface {\n\tname: string\n\tread(): Promise\u003cBuffer | Uint8Array | string | null\u003e // Reads input\n\twrite(data: string): Promise\u003cvoid\u003e // Writes output\n}\n\nclass RPCChannel\u003c\n\tLocalAPI extends Record\u003cstring, any\u003e,\n\tRemoteAPI extends Record\u003cstring, any\u003e,\n\tIo extends IoInterface = IoInterface\n\u003e {}\n```\n\n## Serialization\n\nkkrpc supports two serialization formats for message transmission:\n\n- `json`: Standard JSON serialization\n- `superjson`: Enhanced JSON serialization with support for more data types like Date, Map, Set, BigInt, and Uint8Array (default since v0.2.0)\n\nYou can specify the serialization format when creating a new RPCChannel:\n\n```ts\n// Using default serialization (superjson)\nconst rpc = new RPCChannel(io, { expose: apiImplementation })\n\n// Explicitly using superjson serialization (recommended for clarity)\nconst rpc = new RPCChannel(io, {\n\texpose: apiImplementation,\n\tserialization: { version: \"superjson\" }\n})\n\n// Using standard JSON serialization (for backward compatibility)\nconst rpc = new RPCChannel(io, {\n\texpose: apiImplementation,\n\tserialization: { version: \"json\" }\n})\n```\n\nFor backward compatibility, the receiving side will automatically detect the serialization format so older clients can communicate with newer servers and vice versa.\n\n## Examples\n\nBelow are simple examples.\n\n### Stdio Example\n\n```ts\nimport { NodeIo, RPCChannel } from \"kkrpc\"\nimport { apiMethods } from \"./api.ts\"\n\nconst stdio = new NodeIo(process.stdin, process.stdout)\nconst child = new RPCChannel(stdio, { expose: apiMethods })\n```\n\n```ts\nimport { spawn } from \"child_process\"\n\nconst worker = spawn(\"bun\", [\"scripts/node-api.ts\"])\nconst io = new NodeIo(worker.stdout, worker.stdin)\nconst parent = new RPCChannel\u003c{}, API\u003e(io)\nconst api = parent.getAPI()\n\nexpect(await api.add(1, 2)).toBe(3)\n```\n\n### Web Worker Example\n\n```ts\nimport { RPCChannel, WorkerChildIO, type DestroyableIoInterface } from \"kkrpc\"\n\nconst worker = new Worker(new URL(\"./scripts/worker.ts\", import.meta.url).href, { type: \"module\" })\nconst io = new WorkerChildIO(worker)\nconst rpc = new RPCChannel\u003cAPI, API, DestroyableIoInterface\u003e(io, { expose: apiMethods })\nconst api = rpc.getAPI()\n\nexpect(await api.add(1, 2)).toBe(3)\n```\n\n```ts\nimport { RPCChannel, WorkerParentIO, type DestroyableIoInterface } from \"kkrpc\"\n\nconst io: DestroyableIoInterface = new WorkerChildIO()\nconst rpc = new RPCChannel\u003cAPI, API, DestroyableIoInterface\u003e(io, { expose: apiMethods })\nconst api = rpc.getAPI()\n\nconst sum = await api.add(1, 2)\nexpect(sum).toBe(3)\n```\n\n### HTTP Example\n\nCodesandbox: https://codesandbox.io/p/live/4a349334-0b04-4352-89f9-cf1955553ae7\n\n#### `api.ts`\n\nDefine API type and implementation.\n\n```ts\nexport type API = {\n\techo: (message: string) =\u003e Promise\u003cstring\u003e\n\tadd: (a: number, b: number) =\u003e Promise\u003cnumber\u003e\n}\n\nexport const api: API = {\n\techo: (message) =\u003e {\n\t\treturn Promise.resolve(message)\n\t},\n\tadd: (a, b) =\u003e {\n\t\treturn Promise.resolve(a + b)\n\t}\n}\n```\n\n#### `server.ts`\n\nServer only requires a one-time setup, then it won't need to be touched again.\nAll the API implementation is in `api.ts`.\n\n```ts\nimport { HTTPServerIO, RPCChannel } from \"kkrpc\"\nimport { api, type API } from \"./api\"\n\nconst serverIO = new HTTPServerIO()\nconst serverRPC = new RPCChannel\u003cAPI, API\u003e(serverIO, { expose: api })\n\nconst server = Bun.serve({\n\tport: 3000,\n\tasync fetch(req) {\n\t\tconst url = new URL(req.url)\n\t\tif (url.pathname === \"/rpc\") {\n\t\t\tconst res = await serverIO.handleRequest(await req.text())\n\t\t\treturn new Response(res, {\n\t\t\t\theaders: { \"Content-Type\": \"application/json\" }\n\t\t\t})\n\t\t}\n\t\treturn new Response(\"Not found\", { status: 404 })\n\t}\n})\nconsole.log(`Start server on port: ${server.port}`)\n```\n\n#### `client.ts`\n\n```ts\nimport { HTTPClientIO, RPCChannel } from \"kkrpc\"\nimport { api, type API } from \"./api\"\n\nconst clientIO = new HTTPClientIO({\n\turl: \"http://localhost:3000/rpc\"\n})\nconst clientRPC = new RPCChannel\u003c{}, API\u003e(clientIO, { expose: api })\nconst clientAPI = clientRPC.getAPI()\n\nconst echoResponse = await clientAPI.echo(\"hello\")\nconsole.log(\"echoResponse\", echoResponse)\n\nconst sum = await clientAPI.add(2, 3)\nconsole.log(\"Sum: \", sum)\n```\n\n### Chrome Extension Example\n\n#### `background.ts`\n\n```ts\nimport { ChromeBackgroundIO, RPCChannel } from \"kkrpc\"\nimport type { API } from \"./api\"\n\n// Store RPC channels for each tab\nconst rpcChannels = new Map\u003cnumber, RPCChannel\u003cAPI, {}\u003e\u003e()\n\n// Listen for tab connections\nchrome.runtime.onConnect.addListener((port) =\u003e {\n\tif (port.sender?.tab?.id) {\n\t\tconst tabId = port.sender.tab.id\n\t\tconst io = new ChromeBackgroundIO(tabId)\n\t\tconst rpc = new RPCChannel(io, { expose: backgroundAPI })\n\t\trpcChannels.set(tabId, rpc)\n\n\t\tport.onDisconnect.addListener(() =\u003e {\n\t\t\trpcChannels.delete(tabId)\n\t\t})\n\t}\n})\n```\n\n#### `content.ts`\n\n```ts\nimport { ChromeContentIO, RPCChannel } from \"kkrpc\"\nimport type { API } from \"./api\"\n\nconst io = new ChromeContentIO()\nconst rpc = new RPCChannel\u003cAPI, API\u003e(io, {\n\texpose: {\n\t\tupdateUI: async (data) =\u003e {\n\t\t\tdocument.body.innerHTML = data.message\n\t\t\treturn true\n\t\t}\n\t}\n})\n\n// Get API from background script\nconst api = rpc.getAPI()\nconst data = await api.getData()\nconsole.log(data) // { message: \"Hello from background!\" }\n```\n\n### Tauri Example\n\nCall functions in bun/node/deno processes from Tauri app with JS/TS.\n\nIt allows you to call any JS/TS code in Deno/Bun/Node processes from Tauri app, just like using Electron.\n\nSeamless integration with Tauri's official shell plugin and [unlocked shellx plugin](https://github.com/HuakunShen/tauri-plugin-shellx).\n\n```ts\nimport { RPCChannel, TauriShellStdio } from \"kkrpc/browser\"\nimport { Child, Command } from \"@tauri-apps/plugin-shell\"\n\nconst localAPIImplementation = {\n\tadd: (a: number, b: number) =\u003e Promise.resolve(a + b)\n}\n\nasync function spawnCmd(runtime: \"deno\" | \"bun\" | \"node\") {\n\tlet cmd: Command\u003cstring\u003e\n\tlet process = Child | null = null\n\n\tif (runtime === \"deno\") {\n\t\tcmd = Command.create(\"deno\", [\"run\", \"-A\", scriptPath])\n\t\tprocess = await cmd.spawn()\n\t} else if (runtime === \"bun\") {\n\t\tcmd = Command.create(\"bun\", [scriptPath])\n\t\tprocess = await cmd.spawn()\n\t} else if (runtime === \"node\") {\n\t\tcmd = Command.create(\"node\", [scriptPath])\n\t\tprocess = await cmd.spawn()\n\t} else {\n\t\tthrow new Error(`Invalid runtime: ${runtime}, pick either deno or bun`)\n\t}\n\n\t// monitor stdout/stderr/close/error for debugging and error handling\n\tcmd.stdout.on(\"data\", (data) =\u003e {\n\t\tconsole.log(\"stdout\", data)\n\t})\n\tcmd.stderr.on(\"data\", (data) =\u003e {\n\t\tconsole.warn(\"stderr\", data)\n\t})\n\tcmd.on(\"close\", (code) =\u003e {\n\t\tconsole.log(\"close\", code)\n\t})\n\tcmd.on(\"error\", (err) =\u003e {\n\t\tconsole.error(\"error\", err)\n\t})\n\n\tconst stdio = new TauriShellStdio(cmd.stdout, process)\n\tconst stdioRPC = new RPCChannel\u003ctypeof localAPIImplementation, RemoteAPI\u003e(stdio, {\n\t\texpose: localAPIImplementation\n\t})\n\n\tconst api = stdioRPC.getAPI();\n\tawait api\n\t\t.add(1, 2)\n\t\t.then((result) =\u003e {\n\t\t\tconsole.log(\"result\", result)\n\t\t})\n\t\t.catch((err) =\u003e {\n\t\t\tconsole.error(err)\n\t\t})\n\n\tprocess?.kill()\n}\n```\n\nI provided a sample tauri app in `examples/tauri-demo`.\n\n![Sample Tauri App](https://i.imgur.com/nkDwRHk.png)\n","funding_links":["https://github.com/sponsors/HuakunShen","https://buymeacoffee.com/huakun"],"categories":["Development"],"sub_categories":["Integrations"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkunkunsh%2Fkkrpc","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkunkunsh%2Fkkrpc","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkunkunsh%2Fkkrpc/lists"}