0.1.2 • Published 10 months ago

kkrpc v0.1.2

Weekly downloads
-
License
-
Repository
github
Last release
10 months ago

kkrpc

NPM Version JSR Version GitHub last commit

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.

npm.io npm.io npm.io

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.

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

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 })
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

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)
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.

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.

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

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

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

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!" }
0.1.2

10 months ago

0.1.1

10 months ago

0.1.0

10 months ago

0.0.18

10 months ago

0.0.16

11 months ago

0.0.15

11 months ago

0.0.14

11 months ago

0.0.13

1 year ago

0.0.12

1 year ago

0.0.11

1 year ago

0.0.10

1 year ago

0.0.9

1 year ago

0.0.8

1 year ago

0.0.7

1 year ago

0.0.6

1 year ago

0.0.4

1 year ago

0.0.3

1 year ago

0.0.2

1 year ago