birpc
repository·main·Indexed 20 days ago
https://github.com/antfu-collective/birpcA lightweight (~0.5KB), protocol-agnostic, two-way RPC library for message-based communication such as WebSockets or MessageChannel. It provides type-safe remote function calls with zero dependencies and full TypeScript safety for arguments and return types. Features include support for circular references via custom serializers, a grouping mechanism via createBirpcGroup for one-to-many communication, and a compact JSON-based wire format.
What's inside birpc
- birpc is a message-based, two-way remote procedure call (RPC) library. It allows you to call remote functions as if they were local functions, returning a Promise for the response. It is designed to be protocol-agnostic, meaning it can work over WebSockets, MessageChannel, or any communication medium that supports message passing. It is lightweight (~0.5KB), has zero dependencies, and provides full TypeScript safety for function arguments and return types.
Handle circular references in RPC
mainStandard
JSON.stringifydoes not support circular references. If your RPC data contains circular structures, you should use a serializer likestructured-clone-esinstead of the defaultJSON.stringifyin yourcreateBirpcconfiguration.import { parse, stringify } from 'structured-clone-es' const rpc = createBirpc<ServerFunctions>( functions, { post: data => ws.send(data), on: fn => ws.on('message', fn), // use structured-clone-es as serializer to support circular references serialize: v => stringify(v), deserialize: v => parse(v), }, )Implement RPC using WebSocket
mainWhen using WebSockets, you must provide custom
serializeanddeserializefunctions in thecreateBirpcoptions because WebSockets do not handle object serialization automatically. You also need to map thepostandonproperties to the WebSocket'ssendandon('message', ...)methods.// Client Example import type { ServerFunctions } from './types' const ws = new WebSocket('ws://url') const clientFunctions: ClientFunctions = { hey(name: string) { return `Hey ${name} from client` } } const rpc = createBirpc<ServerFunctions>( clientFunctions, { post: data => ws.send(data), on: fn => ws.on('message', fn), // these are required when using WebSocket serialize: v => JSON.stringify(v), deserialize: v => JSON.parse(v), }, ) await rpc.hi('Client')Implement RPC using MessageChannel
mainUsing
MessageChannelis simpler because it automatically handles message serialization and supports circular references out-of-the-box. You connect the two sides of the RPC by passing different ports (port1andport2) from the sameMessageChannelinstance to the respective client and server configurations.// Setup channel export const channel = new MessageChannel() // Bob (Side A) const Bob: BobFunctions = { hey(name: string) { return `Hey ${name}, I am Bob` } } const rpcBob = createBirpc<AliceFunctions>( Bob, { post: data => channel.port1.postMessage(data), on: fn => channel.port1.on('message', fn), }, ) // Alice (Side B) const Alice: AliceFunctions = { hi(name: string) { return `Hi ${name}, I am Alice` } } const rpcAlice = createBirpc<BobFunctions>( Alice, { post: data => channel.port2.postMessage(data), on: fn => channel.port2.on('message', fn), }, )Broadcast RPC calls to multiple clients using the broadcast object
mainThe
broadcastproperty of aBirpcGroupallows you to invoke functions across all connected clients. Depending on whetherproxifyis enabled, you can call functions directly by name or use built-in utility methods.Built-in Broadcast Methods
These methods are always available on the
broadcastobject:$call(method, ...args): Calls the method on all clients and returns an array of results (one per client).$callOptional(method, ...args): Same as$call, but returnsundefinedfor a client if the method is not defined on their side.$callEvent(method, ...args): Sends the method call as an event (no response expected) to all clients.$callRaw(options): Performs a raw call using aCallRawOptionsobject.
Proxified Broadcast
If
proxify: true(the default), you can call remote functions directly on thebroadcastobject. Each proxified function also gains an.asEvent(...args)method to trigger the call as an event across all clients.// If proxify is true (default) // Calling a remote function directly on broadcast const results = await group.broadcast.hello('World') // Calling a remote function as an event across all clients await group.broadcast.hello.asEvent('World') // Using built-in utility methods const rawResults = await group.broadcast.$call('hello', 'World')How Birpc handles events and requests
mainBirpc distinguishes between standard RPC calls (request-response) and events (fire-and-forget).
- Standard Calls: By default, calling a proxified method sends a
TYPE_REQUEST. The client waits for aTYPE_RESPONSEwith a matching ID (i). If no response arrives within thetimeoutperiod, an error is thrown. - Events: If a method name is listed in
eventNames, or if you use$callEvent/.asEvent, the request is sent without an ID or a requirement for a response. This is useful for one-to-many communication or notifications. - Optional Calls: Using
$callOptionalor setting theoptionalflag in a request tells the server that if the function is missing, it should returnundefinedinstead of an error.
Customizing Request Flow with
onRequestThe
onRequesthook allows you to intercept a request before it is sent. You can use the providednextfunction to continue the standard flow or useresolveto provide a response immediately without hitting the transport layer.const rpc = createBirpc(functions, { onRequest: async (req, next, resolve) => { if (req.m === 'secretMethod') { // Intercept and resolve manually resolve({ data: 'intercepted' }) } else { // Continue standard flow await next() } } })- Standard Calls: By default, calling a proxified method sends a
Configure tsdown for birpc
mainThe
tsdown.config.tsfile is used to configure the build process for the project usingtsdown. The configuration object is created via thedefineConfigfunction and supports the following options:entry: An array of strings specifying the entry point files for the build (e.g.,['src/index.ts']).dts: A boolean that, when set totrue, enables the generation of TypeScript declaration files.exports: A boolean that, when set totrue, enables the generation of package exports.
import { defineConfig } from 'tsdown' export default defineConfig({ entry: [ 'src/index.ts', ], dts: true, exports: true, })Configure Birpc via BirpcOptions
mainBirpcOptionscombinesChannelOptions(transport layer) andEventOptions(RPC behavior). Use these to customize the RPC lifecycle.ChannelOptions
post: Function to post raw messages. Returns aThenable.on: Listener to receive raw messages. Takes a callback(data, ...extras) => void.off: (Optional) Function to clear the listener when$closeis called.serialize: (Optional) Custom function to serialize data before sending.deserialize: (Optional) Custom function to deserialize incoming data.bind:'rpc' | 'functions'. Determines thethiscontext when calling local functions. Defaults to'rpc'.meta: Custom metadata attached to the RPC instance's$metaproperty.
EventOptions
eventNames: Array of method names that should be treated as events (no response expected).timeout: Maximum time to wait for a response in ms. Defaults to60_000.proxify: Whether to proxy remote functions. Iffalse, you must userpc.$call('method', ...args). Defaults totrue.resolver: Custom resolver for advanced function resolution.onRequest: Hook triggered before an event is sent. Allows intercepting or manually resolving requests.onFunctionError: Handler for errors in local functions. Returningtrueprevents the error from being thrown.onGeneralError: Handler for serialization or messaging errors. Returningtrueprevents the error from being thrown.onTimeoutError: Handler for timeout errors. Returningtrueprevents the error from being thrown.
Initialize Birpc with createBirpc
mainUse
createBirpcto establish an RPC connection. You must provide a local functions object (the server-side implementation) and aChannelOptionsobject that defines how messages are sent and received via your transport layer (e.g., WebSocket, MessageChannel).To use Birpc, you need to implement the
postandonmethods inChannelOptionsto bridge the communication between the client and the server.import { createBirpc } from 'birpc' const localFunctions = { hello: (name: string) => `Hello ${name}!` } const rpc = createBirpc(localFunctions, { post: (data) => myTransport.send(data), on: (fn) => myTransport.onMessage(fn), }) // If proxify is true (default), you can call methods directly: const result = await rpc.hello('world')Update RPC channels dynamically with updateChannels
mainIf your
channelsargument increateBirpcGroupwas provided as a function, or if you need to force a refresh of the connected clients, useupdateChannels.Calling
updateChannelswith a callback allows you to modify the channel configuration. It returns a new array ofclientsbased on the updated configuration.// Update channels and get the new clients const newClients = group.updateChannels((channels) => { channels.push({ url: 'ws://new-endpoint:1234' }) })Use Birpc built-in methods and properties
mainThe
BirpcReturnobject provides several built-in utilities for managing the RPC connection and making manual calls. These are available even ifproxifyis set tofalse.Built-in Methods
$call(method, ...args): Calls a remote function and waits for the result.$callOptional(method, ...args): Same as$call, but returnsundefinedif the function is not found on the remote side.$callEvent(method, ...args): Sends an event without expecting a response.$callRaw(options): Calls a method using a raw options object:{ method: string, args: unknown[], event?: boolean, optional?: boolean }.$close(error?): Closes the RPC connection. If an error is provided, it is used to reject pending calls.$rejectPendingCalls(handler?): Rejects all currently pending calls. An optionalhandlercan be used to customize the rejection.
Built-in Properties
$functions: The original local functions object.$closed: A read-only boolean indicating if the RPC is closed.$meta: The custom metadata provided in options.
Event-specific behavior
If a method is included in
eventNames, it will have an.asEventproperty that allows you to send it as an event even if the proxy normally treats it as a call.// Using built-in methods await rpc.$call('methodName', arg1, arg2) await rpc.$callEvent('eventMethod') // Checking status if (rpc.$closed) { console.log('Connection is dead') } // Closing connection rpc.$close()Create a group of RPC functions with createBirpcGroup
mainUse
createBirpcGroupto manage multiple RPC clients simultaneously. This is useful for one-to-many communication patterns where you want to define a set of local functions and broadcast calls to multiple remote channels.createBirpcGroupaccepts:functions: The local functions to be exposed.channels: An array ofChannelOptionsor a function that returns an array ofChannelOptions.options: An optionalEventOptionsobject. Settingproxify: falsein options will disable the automatic proxying of remote functions.
The returned
BirpcGroupobject provides access to the individualclients, the originalfunctions, abroadcastobject for multi-client calls, and anupdateChannelsmethod to dynamically refresh the client list.import { createBirpcGroup } from 'birpc' const functions = { hello: (name: string) => `Hello ${name}` } const group = createBirpcGroup( functions, [{ url: 'ws://localhost:1234' }] ) // Access individual clients const clients = group.clients // Access local functions const local = group.functions