birpc

repository·main·Indexed 20 days ago

https://github.com/antfu-collective/birpc

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

Tokens
4.1K
Snippets
11
Records
13
Agent score
69%

What's inside birpc

  1. What is birpc?

    main
    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.
  2. Handle circular references in RPC

    main

    Standard JSON.stringify does not support circular references. If your RPC data contains circular structures, you should use a serializer like structured-clone-es instead of the default JSON.stringify in your createBirpc configuration.

    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),
      },
    )
  3. Implement RPC using WebSocket

    main

    When using WebSockets, you must provide custom serialize and deserialize functions in the createBirpc options because WebSockets do not handle object serialization automatically. You also need to map the post and on properties to the WebSocket's send and on('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')
  4. Implement RPC using MessageChannel

    main

    Using MessageChannel is 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 (port1 and port2) from the same MessageChannel instance 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),
      },
    )
  5. Broadcast RPC calls to multiple clients using the broadcast object

    main

    The broadcast property of a BirpcGroup allows you to invoke functions across all connected clients. Depending on whether proxify is 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 broadcast object:

    • $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 returns undefined for 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 a CallRawOptions object.

    Proxified Broadcast

    If proxify: true (the default), you can call remote functions directly on the broadcast object. 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')
  6. How Birpc handles events and requests

    main

    Birpc distinguishes between standard RPC calls (request-response) and events (fire-and-forget).

    1. Standard Calls: By default, calling a proxified method sends a TYPE_REQUEST. The client waits for a TYPE_RESPONSE with a matching ID (i). If no response arrives within the timeout period, an error is thrown.
    2. 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.
    3. Optional Calls: Using $callOptional or setting the optional flag in a request tells the server that if the function is missing, it should return undefined instead of an error.

    Customizing Request Flow with onRequest

    The onRequest hook allows you to intercept a request before it is sent. You can use the provided next function to continue the standard flow or use resolve to 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()
        }
      }
    })
  7. Configure tsdown for birpc

    main

    The tsdown.config.ts file is used to configure the build process for the project using tsdown. The configuration object is created via the defineConfig function 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 to true, enables the generation of TypeScript declaration files.
    • exports: A boolean that, when set to true, enables the generation of package exports.
    import { defineConfig } from 'tsdown'
    
    export default defineConfig({
      entry: [
        'src/index.ts',
      ],
      dts: true,
      exports: true,
    })
  8. Configure Birpc via BirpcOptions

    main

    BirpcOptions combines ChannelOptions (transport layer) and EventOptions (RPC behavior). Use these to customize the RPC lifecycle.

    ChannelOptions

    • post: Function to post raw messages. Returns a Thenable.
    • on: Listener to receive raw messages. Takes a callback (data, ...extras) => void.
    • off: (Optional) Function to clear the listener when $close is called.
    • serialize: (Optional) Custom function to serialize data before sending.
    • deserialize: (Optional) Custom function to deserialize incoming data.
    • bind: 'rpc' | 'functions'. Determines the this context when calling local functions. Defaults to 'rpc'.
    • meta: Custom metadata attached to the RPC instance's $meta property.

    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 to 60_000.
    • proxify: Whether to proxy remote functions. If false, you must use rpc.$call('method', ...args). Defaults to true.
    • 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. Returning true prevents the error from being thrown.
    • onGeneralError: Handler for serialization or messaging errors. Returning true prevents the error from being thrown.
    • onTimeoutError: Handler for timeout errors. Returning true prevents the error from being thrown.
  9. Initialize Birpc with createBirpc

    main

    Use createBirpc to establish an RPC connection. You must provide a local functions object (the server-side implementation) and a ChannelOptions object that defines how messages are sent and received via your transport layer (e.g., WebSocket, MessageChannel).

    To use Birpc, you need to implement the post and on methods in ChannelOptions to 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')
  10. Update RPC channels dynamically with updateChannels

    main

    If your channels argument in createBirpcGroup was provided as a function, or if you need to force a refresh of the connected clients, use updateChannels.

    Calling updateChannels with a callback allows you to modify the channel configuration. It returns a new array of clients based on the updated configuration.

    // Update channels and get the new clients
    const newClients = group.updateChannels((channels) => {
      channels.push({ url: 'ws://new-endpoint:1234' })
    })
  11. Use Birpc built-in methods and properties

    main

    The BirpcReturn object provides several built-in utilities for managing the RPC connection and making manual calls. These are available even if proxify is set to false.

    Built-in Methods

    • $call(method, ...args): Calls a remote function and waits for the result.
    • $callOptional(method, ...args): Same as $call, but returns undefined if 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 optional handler can 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 .asEvent property 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()
  12. Create a group of RPC functions with createBirpcGroup

    main

    Use createBirpcGroup to 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.

    createBirpcGroup accepts:

    1. functions: The local functions to be exposed.
    2. channels: An array of ChannelOptions or a function that returns an array of ChannelOptions.
    3. options: An optional EventOptions object. Setting proxify: false in options will disable the automatic proxying of remote functions.

    The returned BirpcGroup object provides access to the individual clients, the original functions, a broadcast object for multi-client calls, and an updateChannels method 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