JSON-RPC 2.0

repository·main·Indexed 42 days ago

https://github.com/shogowada/json-rpc-2.0

A lightweight, protocol-agnostic implementation of the JSON-RPC 2.0 specification for Node.js and browser environments. Version 1.8.0 provides tools to implement clients and servers using JSONRPCClient and JSONRPCServer, including support for bi-directional communication via JSONRPCServerAndClient. It features first-class TypeScript support with TypedJSONRPCServer and TypedJSONRPCClient for strongly typed method calls, zero external dependencies, and advanced APIs for raw message handling.

Tokens
9.8K
Snippets
36
Records
37
Agent score
50%

What's inside json-rpc-2.0

  1. Overview of json-rpc-2.0

    main

    The json-rpc-2.0 library enables client and server communication using function calls compliant with the JSON-RPC 2.0 specification.

    Key features include:

    • Protocol Agnostic: Can be used over HTTP, WebSocket, TCP, UDP, inter-process communication, or any other transport layer. This makes migrating between protocols (e.g., from HTTP to WebSocket) straightforward.
    • Zero External Dependencies: The package is lightweight and avoids dependency conflicts.
    • Cross-Platform: Compatible with both browser and Node.js environments.
    • First-class TypeScript Support: Written in TypeScript with support for strongly typed client and server calls.
  2. Implement Bi-directional JSON-RPC with JSONRPCServerAndClient

    main

    For full-duplex communication (like WebSockets), use JSONRPCServerAndClient. This class combines a server and a client into a single entity.

    1. Initialize it with a JSONRPCServer and a JSONRPCClient.
    2. Use serverAndClient.receiveAndSend(parsedMessage) inside your transport's message handler (e.g., ws.onmessage) to process incoming messages and automatically send responses.
    3. Use serverAndClient.rejectAllPendingRequests(reason) when the connection closes to prevent client-side requests from hanging.
    const serverAndClient = new JSONRPCServerAndClient(
      new JSONRPCServer(),
      new JSONRPCClient((request) => {
        webSocket.send(JSON.stringify(request));
        return Promise.resolve();
      })
    );
    
    webSocket.onmessage = (event) => {
      serverAndClient.receiveAndSend(JSON.parse(event.data.toString()));
    };
    
    webSocket.onclose = (event) => {
      serverAndClient.rejectAllPendingRequests(`Connection closed: ${event.reason}`);
    };
  3. Implement a JSON-RPC Server with JSONRPCServer

    main

    Use JSONRPCServer to create a server that handles JSON-RPC requests. You can register methods using addMethod(methodName, handler). The handler receives the request parameters as its first argument and can optionally receive custom injected parameters as its second argument.

    To process incoming requests, use server.receive(jsonRPCRequest) or server.receiveJSON(jsonString). If the request is a notification (no id), receive returns undefined (or a falsy value), allowing you to respond with a 204 No Content status in your transport layer (e.g., Express).

    const { JSONRPCServer } = require("json-rpc-2.0");
    
    const server = new JSONRPCServer();
    
    // Basic method
    server.addMethod("echo", ({ text }) => text);
    
    // Method with custom injected parameters (e.g., for authentication)
    server.addMethod("echo", ({ text }, { userID }) => `${userID} said ${text}`);
    
    // Processing a request
    server.receive(jsonRPCRequest, { userID }).then((jsonRPCResponse) => {
      if (jsonRPCResponse) {
        res.json(jsonRPCResponse);
      } else {
        res.sendStatus(204);
      }
    });
  4. Handle errors in JSON-RPC

    main

    Server-side Error Handling

    • Standard Errors: Rejecting a method with a standard JavaScript Error will result in a JSON-RPC error response with the error's message.
    • Custom JSON-RPC Errors: Use JSONRPCErrorException to return specific error codes and data.
    • Error Mapping: Implement server.mapErrorToJSONRPCErrorResponse to globally transform any thrown error into a specific JSONRPCErrorResponse format.

    Client-side Error Handling

    When a server returns an error, the client.request promise will reject with an Error object containing the message from the server.

    import { JSONRPCErrorException } from "json-rpc-2.0";
    
    // Throwing a structured error on the server
    server.addMethod("throws", () => {
      throw new JSONRPCErrorException("Invalid input", 123, { foo: "bar" });
    });
    
    // Catching error on the client
    client.request("throws").catch((error) => {
      console.error(error.message); 
    });
  5. Implement a JSON-RPC Client with JSONRPCClient

    main

    Use JSONRPCClient to make requests to a JSON-RPC server. The constructor requires a transport function that takes a jsonRPCRequest and returns a Promise. This function is responsible for sending the request and, upon receiving a response, calling client.receive(jsonRPCResponse) to resolve the client's internal request promise.

    • client.request(method, params, [customParams]): Returns a promise that resolves to the result of the method call.
    • client.notify(method, params, [customParams]): Sends a notification (no response expected).
    import { JSONRPCClient } from "json-rpc-2.0";
    
    const client = new JSONRPCClient((jsonRPCRequest) =>
      fetch("http://localhost/json-rpc", {
        method: "POST",
        headers: { "content-type": "application/json" },
        body: JSON.stringify(jsonRPCRequest),
      }).then((response) => {
        if (response.status === 200) {
          return response.json().then((jsonRPCResponse) => client.receive(jsonRPCResponse));
        }
        // Handle error cases...
      })
    );
    
    // Making a request
    client.request("echo", { text: "Hello" }).then(console.log);
    
    // Making a notification
    client.notify("log", { message: "Hello" });
  6. Implement Typed client and server

    main

    Use TypedJSONRPCServer<T> and TypedJSONRPCClient<T> to provide strong type safety for your JSON-RPC methods.

    1. Define a type (not an interface) representing your methods, where each key is a method name and the value is a function signature defining params and the return type.
    2. Pass this type to the constructor of the server and client.

    This enables compile-time checking for method names, parameter shapes, and return types for both addMethod on the server and request/notify on the client.

    type Methods = {
      echo(params: { message: string }): string;
      sum(params: { x: number; y: number }): number;
    };
    
    const server: TypedJSONRPCServer<Methods> = new JSONRPCServer();
    const client: TypedJSONRPCClient<Methods> = new JSONRPCClient(/* ... */);
    
    // Type-safe usage
    server.addMethod("echo", ({ message }) => message); // OK
    // server.addMethod("echo", ({ message }) => 123); // Error: return type must be string
    
    client.request("sum", { x: 1, y: 2 }).then(result => {
      // result is inferred as number
    });
  7. Use Advanced APIs for raw JSON-RPC messages

    main

    If you need full control over the JSON-RPC wire format, use the Advanced API methods:

    Server

    server.addMethodAdvanced(methodName, handler) allows you to handle the raw JSONRPCRequest object and return a raw JSONRPCResponse object directly. This is useful for custom validation or complex response structures.

    Client

    client.requestAdvanced(jsonRPCRequest) allows you to send a manually constructed JSONRPCRequest and receive a JSONRPCResponse. You can also provide a custom ID factory function to the JSONRPCClient constructor to avoid ID conflicts between basic and advanced calls.

    // Server Advanced Method
    server.addMethodAdvanced("doSomething", (jsonRPCRequest: JSONRPCRequest): PromiseLike<JSONRPCResponse> => {
      return {
        jsonrpc: JSONRPC,
        id: jsonRPCRequest.id,
        result: "Success",
      };
    });
    
    // Client Advanced Request
    client.requestAdvanced(jsonRPCRequest).then((response) => {
      if (response.error) {
        console.log(response.error.message);
      } else {
        console.log(response.result);
      }
    });
  8. Add timeouts to JSONRPCClient requests

    main

    You can prevent requests from hanging indefinitely by using the .timeout(ms, [errorFactory]) method on the client. If the response is not received within the specified milliseconds, the request promise will reject.

    You can optionally provide a custom error factory function to define the specific JSON-RPC error response returned upon timeout.

    client
      .timeout(10 * 1000) // Fails after 10 seconds
      .request("echo", { text: "Hello" });
    
    // With custom error response
    client
      .timeout(10 * 1000, (id) => createJSONRPCErrorResponse(id, 123, "Timeout error"))
      .request("echo", { text: "Hello" });
  9. Configure JSONRPCServer middleware and options

    main

    Middleware

    Use server.applyMiddleware(middleware1, middleware2, ...) to intercept requests and responses. Middleware functions follow the signature (next, request, serverParams) => Promise<Response>. The next function must be called to proceed to the next middleware or the method handler.

    Constructor Options

    You can pass an options object to the JSONRPCServer constructor to configure an errorListener:

    new JSONRPCServer({
      errorListener: (message: string, data: unknown): void => {
        // Custom error logging logic
      },
    });
    const logMiddleware = (next, request, serverParams) => {
      console.log(`Received ${JSON.stringify(request)}`);
      return next(request, serverParams).then((response) => {
        console.log(`Responding ${JSON.stringify(response)}`);
        return response;
      });
    };
    
    server.applyMiddleware(logMiddleware);