RPC Anywhere

repository·main·Indexed 42 days ago

https://github.com/daniguardiola/rpc-anywhere

A transport-agnostic, type-safe RPC library for creating connections between different execution contexts, such as iframes, web workers, or Electron processes. Version 1.7.0 provides high-fidelity type safety using RPCSchema to define requests and messages, decoupling RPC logic from the underlying transport layer.

Tokens
17.4K
Snippets
61
Records
72
Agent score
53%

What's inside RPC Anywhere

  1. Use EmptyRPCSchema for pure client/server connections

    main

    If one endpoint only acts as a client (sends requests but doesn't handle any) or only as a server (handles requests but doesn't send messages), use EmptyRPCSchema for the non-participating side. This simplifies the type definition.

    type RemoteSchema = RPCSchema<{ requests: { requestName: void; }; }>;
    
    // rpc-local.ts (client)
    const rpc = createRPC<EmptyRPCSchema, RemoteSchema>();
    
    // rpc-remote.ts (server)
    const rpc = createRPC<RemoteSchema, EmptyRPCSchema>({
      requestHandler: {
        requestName() { /* ... */ },
      },
    });
  2. How RPC Anywhere works: Concepts

    main

    RPC Anywhere facilitates communication between two endpoints using a transport layer. It is transport-agnostic, meaning the logic of your RPC is decoupled from how messages move (e.g., via Iframes, Web Workers, or Electron IPC).

    There are two primary ways to exchange information:

    1. Requests: Messages sent from one endpoint to another that expect a response (similar to a function call).
    2. Messages: One-way messages sent without expecting a response.

    Unlike traditional client-server models, RPC Anywhere is flexible: both endpoints can act as both senders and receivers of requests and messages.

  3. Understand RPC Anywhere type safety and features

    main

    RPC Anywhere provides high-fidelity type safety by deriving all API surfaces from your schema definitions. Key features include:

    • Strictly Typed Data: Request parameters, response data, and message contents are strictly typed based on the schema.
    • Handler Validation: You cannot return the wrong response type from a request handler, and message listeners are strictly typed.
    • Proxy API Safety: The proxy APIs for requests and messages are fully typed; attempting to call a non-existent request or sending invalid data types will result in a TypeScript error.
    • Schema Inference: Request schema types can be inferred from runtime request handlers, creating a single source of truth.
    • Constrained API Surface: The available methods on an RPC instance are dynamically constrained by the schema. For example:
      • If a remote schema has no requests, the request method is unavailable.
      • If a local schema has no requests, you cannot set a request handler or customize maximumRequestTime.
  4. Handle message collisions in shared channels

    main

    If your transport uses a shared communication medium (for example, window.addEventListener('message', ...) in a browser), it may receive messages intended for other libraries or user code.

    To prevent your RPC instance from processing irrelevant messages, you should implement a filtering mechanism. Common strategies include:

    • Including a unique ID within the message payload.
    • Checking contextual metadata (e.g., the origin property of a MessageEvent).

    Built-in transports in rpc-anywhere use unique IDs or custom filtering functions to solve this. You can use the utilities provided in src/transport-utils.ts to implement similar logic in your custom transport.

  5. How bridging transports works

    main

    RPC Anywhere allows you to connect disparate communication channels by 'bridging' them. A bridge acts as a relay that forwards messages from one transport to another in both directions.

    This is useful for multi-hop communication scenarios where an RPC endpoint (like an iframe) needs to communicate with a remote server through intermediate layers (like an Electron Main process and a Renderer process).

    Key Concepts:

    • Endpoints: You only create RPC instances (using createRPC) at the actual start and end points of the communication chain.
    • Bridges: You use createTransportBridge at the intermediate stages to link two transports together. Bridges do not require their own RPC instances.
    • Lifecycle: Bridges can be started with .start() and stopped with .stop(). Calling .stop() unregisters event listeners for transports that support it.
  6. Infer the RPC schema from a request handler

    main

    To avoid duplicating your schema definition and your handler implementation, you can use createRPCRequestHandler to create a handler and then use typeof to infer the schema type. This ensures your schema stays in sync with your actual logic.

    import { createRPCRequestHandler, RPCSchema, createRPC } from 'rpc-anywhere';
    
    const myRequestHandler = createRPCRequestHandler({
      myRequest({ a, b }: { a: number; b: string }) {
        return { c: a > 0 && b.length > 0 };
      },
    });
    
    // Infer schema from the handler
    type Schema = RPCSchema<
      { messages: { myMessage: void } }, 
      typeof myRequestHandler
    >;
    
    const rpc = createRPC<Schema>({
      // ...
      requestHandler: myRequestHandler,
    });
  7. Document RPC schemas with JSDoc

    main

    You can add JSDoc comments to requests, parameters, responses, and messages within your RPCSchema definition. These comments will appear in IDEs (like VS Code) when hovering over the corresponding symbols during RPC usage, providing helpful context and examples.

    type MySchema = RPCSchema<{
      requests: {
        /**
         * Move the car.
         * @example
         * ```
         * const result = await rpc.request.move({ direction: "left", duration: 1000 });
         * ```
         */
        move: {
          params: {
            /** The direction of the movement. */
            direction: "left" | "right";
            /** The total duration of the movement. */
            duration: number;
            /** The velocity of the car. @default 100 */
            velocity?: number;
          };
          response: {
            /** The total distance traveled by the car. */
            distance: number;
          };
        };
      };
    }>;
  8. How transport bridges work

    main

    A transport bridge is a mechanism that connects two RPCTransport instances to each other. It creates a bidirectional communication path where messages sent to one transport are automatically forwarded to the other. This is useful for linking different communication layers (e.g., connecting a WebSocket transport to a local event emitter transport).

    // Conceptually, a bridge connects two transports:
    // transportA -> (bridge) -> transportB
    // transportB -> (bridge) -> transportA
  9. Implement an RPC Request Handler

    main

    A request handler processes incoming RPC calls. You can implement it in two ways:

    1. Function Form (RPCRequestHandlerFn): A single function that uses a switch or if block on the method name to route calls.
    2. Object Form (RPCRequestHandlerObject): An object where each key is a method name and the value is the implementation function. This form supports a fallback method _ to handle unknown requests.

    When using the object form, the types for params and response are automatically inferred from the function signatures.

    import { type RPCRequestHandler } from "./rpc-anywhere";
    
    type MyRequests = {
      add: { params: { a: number; b: number }; response: number };
    };
    
    const handler: RPCRequestHandler<MyRequests> = {
      add: async ({ a, b }) => a + b,
      _: (method, params) => console.log(`Unknown method: ${method}`),
    };