Migrate from 0.7.x to 1.0
kkrpc 1.0 is a breaking rewrite of the public API. The stable package now uses native Transport<RPCMessage> objects everywhere. The old classic adapter model based on IoInterface, IoMessage, and public *IO classes is not preserved.
This guide is for applications currently using kkrpc 0.7.x or the temporary vNext entries and moving to kkrpc 1.0.
What Changed
Section titled “What Changed”The 1.0 API promotes the previous native architecture into the stable kkrpc package entry.
Major changes:
kkrpcnow exports the stable native core:RPCChannel,wrap(),expose(),dispose(),transfer(), and core protocol/transport/plugin types.- Runtime transports live behind subpath exports such as
kkrpc/worker,kkrpc/stdio,kkrpc/http,kkrpc/ws, andkkrpc/electron. - Optional integrations and peer dependencies are no longer pulled through the main entry.
- Validation and middleware are plugins, not top-level classic channel options.
- Request metadata for tracing and logging is configured with
getMetadataand read from plugin or middlewarectx.meta. - SuperJSON is an opt-in codec feature, not a core dependency.
- Async iterable streaming and request/response remote references are opt-in entries. See Migrate from v0.1.0 to v0.2.0 for the feature split.
- HTTP is explicitly unary request/response. Use WebSocket or another evented transport for bidirectional calls and callback arguments.
- Temporary
kkrpc/nextentries were removed because the native API is now stable.
Quick Checklist
Section titled “Quick Checklist”- Upgrade the package to
kkrpc@1. - Replace old imports with stable imports from
kkrpcand transport subpaths. - Replace classic IO adapters with native transport factories.
- Use
wrap()for client-only proxies andexpose()for server-only APIs. - Use
new RPCChannel(transport, { expose })when both sides expose APIs. - Replace
validatorsandinterceptorschannel options withvalidationPlugin()andmiddlewarePlugin(). - Move trace, logging, or activity context to request metadata. See Request Metadata.
- Move SuperJSON usage to
kkrpc/superjsonand compose it throughcreateTransport()only where needed. - Remove temporary
kkrpc/next,classic-compat,next/io,browser-lite,browser-mini, andelectron-ipcimports. - Run type checks and runtime tests for every migrated transport boundary.
Removed Public APIs
Section titled “Removed Public APIs”These names and entries should not remain in 1.0 applications.
| Removed | Use instead |
|---|---|
IoInterface, IoMessage | Transport<RPCMessage> |
Public *IO adapter classes | Native transport factories, such as workerTransport() or webSocketTransport() |
Classic validators option | validationPlugin() from kkrpc/validation |
Classic interceptors option | middlewarePlugin() from kkrpc/middleware |
RPCValidators, classic validation helpers | defineMethod(), defineAPI(), extractValidators(), validationPlugin() |
RPCInterceptor from the old API | MiddlewareHandler from kkrpc/middleware |
kkrpc/next and kkrpc/next/* | Stable kkrpc and stable subpaths |
kkrpc/next/classic-compat | Native plugins and options |
kkrpc/next/io | Native transport implementations |
kkrpc/browser-lite | kkrpc or kkrpc/browser |
kkrpc/browser-mini | kkrpc or kkrpc/browser |
kkrpc/electron-ipc | kkrpc/electron |
Do not add compatibility bridges in new 1.0 code. If an old custom adapter still exists, migrate the adapter itself to Transport<RPCMessage>.
Entry Points
Section titled “Entry Points”The main entry is intentionally small and browser-safe. Runtime-specific code and optional peers live behind subpaths.
| Entry | Purpose |
|---|---|
kkrpc | Stable core: RPCChannel, wrap, expose, dispose, transfer, core types |
kkrpc/browser | Explicit browser-safe core entry |
kkrpc/deno | Deno-friendly core entry |
kkrpc/transport | Transport, Platform, Codec, createTransport() |
kkrpc/codecs | Built-in object, JSON, and JSON-line codecs |
kkrpc/plugins | Core plugin types and helpers |
kkrpc/validation | Standard Schema validation plugin and schema helpers |
kkrpc/middleware | Interceptor middleware plugin |
kkrpc/superjson | SuperJSON codec helpers |
kkrpc/streaming | Opt-in async iterable streaming channel |
kkrpc/remote-refs | Opt-in explicit proxy() remote references |
kkrpc/worker | Web Worker transports |
kkrpc/stdio | Node.js, Deno, and Bun stdio transports |
kkrpc/http | HTTP client transport and request handler |
kkrpc/ws | Plain WebSocket transports |
kkrpc/ws/hono | Hono WebSocket integration |
kkrpc/ws/elysia | Elysia WebSocket integration |
kkrpc/iframe | iframe postMessage transports |
kkrpc/chrome-extension | Chrome extension port transport |
kkrpc/electron | Electron IPC and utility process transports |
kkrpc/tauri | Tauri shell stdio transport |
kkrpc/socketio | Socket.IO transport |
kkrpc/rabbitmq | RabbitMQ transport |
kkrpc/kafka | Kafka transport |
kkrpc/redis-streams | Redis Streams transport |
kkrpc/nats | NATS transport |
kkrpc/relay | Transport relay helper |
kkrpc/inspector | Native inspector helpers |
Core API Migration
Section titled “Core API Migration”Client-only calls
Section titled “Client-only calls”In 1.0, the common client path is wrap(remoteTransport).
import { wrap } from "kkrpc"import { webSocketClientTransport } from "kkrpc/ws"import type { API } from "./server"
const api = wrap<API>(webSocketClientTransport({ url: "ws://localhost:3000" }))
console.log(await api.greet("World"))Keep the proxy and call dispose(api) when the client lifetime ends.
import { dispose, wrap } from "kkrpc"
const api = wrap<API>(transport)
dispose(api)Server-only exposure
Section titled “Server-only exposure”Expose a local object with expose(api, transport). The returned controller owns the channel.
import { expose } from "kkrpc"import { webSocketTransport } from "kkrpc/ws"
const controller = expose(api, webSocketTransport(socket))
controller.dispose()Bidirectional channels
Section titled “Bidirectional channels”Use RPCChannel directly when both sides expose APIs or when explicit channel ownership is useful.
import { RPCChannel } from "kkrpc"
const channel = new RPCChannel<LocalAPI, RemoteAPI>(transport, { expose: localAPI })const remote = channel.getAPI()
await remote.ping()channel.destroy()Type parameters are ordered as local API first, remote API second.
Transport Migration
Section titled “Transport Migration”The old adapter layer exposed IO-like objects. The new layer exposes Transport<RPCMessage> factories. Pass the native transport directly into wrap(), expose(), or RPCChannel.
Web Worker
Section titled “Web Worker”Use workerTransport() on the parent side and workerSelfTransport() inside the worker.
import { wrap } from "kkrpc"import { workerTransport } from "kkrpc/worker"
const worker = new Worker(new URL("./worker.ts", import.meta.url), { type: "module" })const api = wrap<WorkerAPI>(workerTransport(worker))import { expose } from "kkrpc"import { workerSelfTransport } from "kkrpc/worker"
const api = { async ping() { return "pong" }}
export type WorkerAPI = typeof api
expose(api, workerSelfTransport())Worker transports can advertise transferable support. Use transfer(value, transferables) only when the underlying platform supports ownership transfer.
Use nodeStdioTransport(), denoStdioTransport(), bunStdioTransport(), or stdioJsonTransport() from kkrpc/stdio.
import { expose } from "kkrpc"import { nodeStdioTransport } from "kkrpc/stdio"
expose(api, nodeStdioTransport({ readable: process.stdin, writable: process.stdout }))import { spawn } from "node:child_process"import { wrap } from "kkrpc"import { nodeStdioTransport } from "kkrpc/stdio"
const child = spawn("node", ["child.js"])const api = wrap<ChildAPI>(nodeStdioTransport({ readable: child.stdout, writable: child.stdin }))Stdio transports use newline-delimited JSON. Do not reuse old blocking read()/write() IO classes.
HTTP in 1.0 is unary request/response only. It is appropriate for normal client-initiated value calls, but not server-initiated calls, callback arguments, async iterable streams, remote-reference handles, or raw function values.
import { createHttpHandler } from "kkrpc/http"
const handler = createHttpHandler(api)
Bun.serve({ fetch(request) { return new URL(request.url).pathname === "/rpc" ? handler(request) : new Response("Not found", { status: 404 }) }})import { wrap } from "kkrpc"import { httpClientTransport } from "kkrpc/http"
const api = wrap<API>(httpClientTransport({ url: "http://localhost:3000/rpc" }))If old HTTP code depended on callbacks, subscriptions, streaming progress, remote references, or server pushes, migrate that boundary to WebSocket or another evented transport instead.
WebSocket
Section titled “WebSocket”Use webSocketTransport(socket) for accepted sockets and webSocketClientTransport({ url }) for clients.
import { expose, wrap } from "kkrpc"import { webSocketClientTransport, webSocketTransport } from "kkrpc/ws"Framework helpers moved to dedicated subpaths:
- Hono:
createHonoWebSocketHandler()andhonoWebSocketTransport()fromkkrpc/ws/hono. - Elysia:
createElysiaWebSocketHandler()andelysiaWebSocketTransport()fromkkrpc/ws/elysia.
iframe and Chrome extension
Section titled “iframe and Chrome extension”Use iframeParentTransport(), iframeChildTransport(), and readiness helpers from kkrpc/iframe. Use chromePortTransport() from kkrpc/chrome-extension for chrome.runtime.Port boundaries.
These transports use message events. Keep origin checks and extension permission boundaries in application code.
Electron
Section titled “Electron”Use kkrpc/electron, not kkrpc/electron-ipc.
Available helpers include:
electronIpcTransport()for endpoint-like IPC messaging.electronUtilityProcessTransport()for parent-side utility process communication.electronUtilityProcessChildTransport()for child-side utility process communication.createSecureIpcBridge()for preload-safe bridge construction.
Do not import Electron-specific helpers from the main kkrpc entry.
Use tauriShellStdioTransport() from kkrpc/tauri for Tauri shell child process communication. Keep Tauri plugin dependencies behind this subpath.
Socket.IO
Section titled “Socket.IO”Use socketIoTransport(socket) from kkrpc/socketio. Socket.IO remains separate from kkrpc/ws because it uses Socket.IO-specific event semantics and peer dependencies.
Message buses
Section titled “Message buses”Use the native message-bus transports from their dedicated subpaths:
| System | 1.0 helper |
|---|---|
| RabbitMQ | rabbitMqTransport() from kkrpc/rabbitmq |
| Kafka | kafkaTransport() from kkrpc/kafka |
| Redis Streams | redisStreamsTransport() from kkrpc/redis-streams |
| NATS | natsTransport() from kkrpc/nats |
Message-bus transports use envelope metadata for peer identity and routing. They may provide at-least-once delivery depending on the broker. Do not assume exactly-once execution unless your application protocol handles idempotency. Configure remotePeerId for point-to-point streaming or remote-reference APIs; broadcast mode is for fan-out messages and does not advertise remote-reference support.
Validation Migration
Section titled “Validation Migration”Validation is now an explicit plugin. It accepts Standard Schema-compatible validators, including Zod, Valibot, and ArkType schemas.
import { expose } from "kkrpc"import { defineAPI, defineMethod, extractValidators, validationPlugin } from "kkrpc/validation"import { z } from "zod"
const api = defineAPI({ add: defineMethod( { input: z.tuple([z.number(), z.number()]), output: z.number() }, async (a, b) => a + b )})
expose(api, transport, { plugins: [validationPlugin(extractValidators(api))]})If old code passed validators directly to a classic channel, move those schemas into a validator map or define the API with defineMethod().
Middleware Migration
Section titled “Middleware Migration”Middleware is also a plugin.
import { expose } from "kkrpc"import { middlewarePlugin, type MiddlewareHandler } from "kkrpc/middleware"
const logger: MiddlewareHandler = async (ctx, next) => { console.log("rpc:start", ctx.method) const result = await next() console.log("rpc:end", ctx.method) return result}
expose(api, transport, { plugins: [middlewarePlugin([logger])]})If old code used classic interceptors, migrate each interceptor to the new MiddlewareHandler context and install it with middlewarePlugin().
Metadata Migration
Section titled “Metadata Migration”If your 0.7 code used getMetadata or interceptor ctx.meta for tracing, activity IDs, or logging correlation, keep that context as request metadata in 1.0. The option remains getMetadata, but it now belongs to the native wrap(), expose(), or RPCChannel options.
import { wrap } from "kkrpc"
const api = wrap<API>(transport, { getMetadata: () => ({ traceparent: currentTraceparent(), requestId: currentRequestId(), sessionId: currentSessionId() })})Receive-side plugins and middleware read the metadata from ctx.meta.
import { middlewarePlugin, type MiddlewareHandler } from "kkrpc/middleware"
const logger: MiddlewareHandler = async (ctx, next) => { console.log("rpc", ctx.method, ctx.meta?.requestId) return await next()}
const plugins = [middlewarePlugin([logger])]See Request Metadata for tracing, logging, and kunkun migration examples.
SuperJSON Migration
Section titled “SuperJSON Migration”SuperJSON is opt-in through kkrpc/superjson and transport composition.
import { createTransport } from "kkrpc/transport"import { superJsonCodec } from "kkrpc/superjson"
const transport = createTransport({ platform, codec: superJsonCodec() })Do not import SuperJSON helpers from kkrpc. This keeps the core bundle small and avoids pulling SuperJSON into applications that do not need it.
Protocol and Interop Notes
Section titled “Protocol and Interop Notes”The stable wire protocol uses compact JSON-compatible records:
{ "t": "q", "id": "request-id", "op": "call", "p": ["math", "add"], "a": [1, 2] }Responses use t: "r"; callback invocations use t: "cb". Async iterable streams use t: "sq" credit/control requests and t: "sr" data/completion/error responses. Explicit remote references use op: "ref" requests inside kkrpc/remote-refs; default-core endpoints reject those operations with an opt-in error. Non-TypeScript implementations should use the language interop references and the skills/interop guide.
Common Pitfalls
Section titled “Common Pitfalls”| Problem | Fix |
|---|---|
Importing runtime transports from kkrpc | Import from runtime subpaths such as kkrpc/ws or kkrpc/electron |
Leaving kkrpc/next imports in code | Replace them with kkrpc and stable subpaths |
| Migrating HTTP code that used callbacks | Use WebSocket or another evented transport |
Assuming wrap() disposes automatically | Keep the proxy and call dispose(proxy) when the lifetime ends |
| Pulling SuperJSON into every bundle | Use kkrpc/superjson only at boundaries that need richer values |
Preserving old *IO class names in wrappers | Rename wrappers around native Transport<RPCMessage> factories |
| Treating transferables as universal | Check transport capabilities and use transfer() only where supported |
Verification
Section titled “Verification”For this repository, run:
pnpm --filter kkrpc check-typespnpm --filter kkrpc testpnpm --filter "./examples/*" check-typesFor downstream projects, run the equivalent TypeScript type check, unit tests, and at least one integration test for each migrated transport boundary.
Search for old API remnants before finishing:
rg 'kkrpc/next|next/io|classic-compat|IoInterface|IoMessage|RPCValidators|kkrpc/browser-lite|kkrpc/browser-mini|kkrpc/electron-ipc'rg '[A-Za-z0-9_]+IO\b'The second search can produce false positives for application names. Review each match and remove old public kkrpc adapter usage.