socket-rpc
is a powerful command-line tool that automatically generates a type-safe RPC (Remote Procedure Call) layer for your client and server applications using socket.io
. It takes a TypeScript interface as input and generates all the necessary code for you to communicate between your client and server with full type safety. It's unopinionated, meaning it only generates the function bindings and doesn't interfere with your existing socket.io
configuration.
- Type-Safe: Full static type checking for your RPC calls, powered by TypeScript.
- Auto-generation: Automatically generates client and server code from a single TypeScript interface definition.
- Unopinionated: Generates only the type-safe bindings, leaving you in full control of your
socket.io
setup. - Bidirectional Communication: Supports both client-to-server and server-to-client RPC calls.
- Simple to Use: Get started with a single command.
- Error Handling: Built-in error handling with
RpcError
type.
The example
directory in this repository is a great starting point and can be used as a template to bootstrap your own project. It demonstrates a practical project structure that you can adapt for your needs.
Create a TypeScript file (e.g., pkg/rpc/define.ts
) that defines the functions your server and client will expose.
// pkg/rpc/define.ts
/**
* Interface defining the functions available on the RPC server
* These functions can be called remotely by RPC clients
*/
interface ServerFunctions {
/**
* Generates text based on the provided prompt
*/
generateText: (prompt: string) => string;
}
/**
* Interface defining the functions available on the RPC client
* These functions can be called by the RPC server to interact with the client
*/
interface ClientFunctions {
/**
* Displays an error to the client user interface
*/
showError: (error: Error) => void;
/**
* Asks the client a question and expects a response.
* @param question The question to ask the client.
* @returns The client's answer to the question.
*/
askQuestion: (question: string) => string;
}
**Important Note:** Do not use `Promise` in the return types when defining functions in your interfaces (`ServerFunctions`, `ClientFunctions`). The library automatically wraps the return types in `Promise`. The implementation of these functions can be `async` and return a `Promise`, but the definition should specify the final resolved type. For example, use `(prompt: string) => string` instead of `(prompt: string) => Promise<string>`.
Use the socketrpc-gen
CLI to generate the RPC code. The generator automatically infers the output directory from the input file path.
bunx socketrpc-gen <path-to-your-interface-file> [options]
For example:
bunx socketrpc-gen ./example/pkg/rpc/define.ts
This will generate a new package in the example/pkg/rpc
directory containing the generated client and server code.
sequenceDiagram
participant ClientApp as "Your Client Application"
participant GenClient as "Generated Client-Side RPC"
participant GenServer as "Generated Server-Side RPC"
participant ServerApp as "Your Server Application"
title socket-rpc: Bidirectional Communication Flow
ClientApp->>GenClient: 1. Calls `generateText("hello")`
activate GenClient
GenClient->>GenServer: 2. Emits "rpc:generateText" event over network
deactivate GenClient
activate GenServer
GenServer->>ServerApp: 3. Invokes your `generateText` handler
activate ServerApp
Note over ServerApp: Server logic decides to<br/>call a function on the client
ServerApp->>GenServer: 4. Calls `askQuestion("Favorite color?")`
GenServer->>GenClient: 5. Emits "rpc:askQuestion" event over network
deactivate GenServer
activate GenClient
GenClient->>ClientApp: 6. Invokes your `askQuestion` handler
activate ClientApp
ClientApp-->>GenClient: 7. Returns answer: "blue"
deactivate ClientApp
GenClient-->>GenServer: 8. Sends response ("blue") back to server
deactivate GenClient
activate GenServer
GenServer-->>ServerApp: 9. `askQuestion` promise resolves with "blue"
Note over ServerApp: Server finishes its logic and<br/>returns the final result
ServerApp-->>GenServer: 10. Returns final result for `generateText`
deactivate ServerApp
GenServer-->>GenClient: 11. Sends final result back to client
deactivate GenServer
activate GenClient
GenClient-->>ClientApp: 12. Original `generateText` promise resolves
deactivate GenClient
Implement the server-side functions and use the generated handlers to process client requests.
// pkg/server/index.ts
import { createServer } from "http";
import { Server } from "socket.io";
import {
handleGenerateText,
showError,
askQuestion,
} from "@socket-rpc/rpc/server.generated";
import { RpcError, isRpcError } from "@socket-rpc/rpc";
const httpServer = createServer();
const io = new Server(httpServer);
io.on("connection", async (socket) => {
// Handle the `generateText` RPC call from the client
handleGenerateText(
socket,
async (prompt: string): Promise<string | RpcError> => {
// Example of server calling a client function and waiting for a response
try {
const clientResponse = await askQuestion(
socket,
"What is your favorite color?",
3000
); // 3s timeout
if (isRpcError(clientResponse)) {
console.error("Client returned an error:", clientResponse.message);
} else {
console.log(`Client's favorite color is: ${clientResponse}`);
}
} catch (e) {
console.error("Did not get a response from client for askQuestion", e);
}
// Example of server calling a fire-and-forget client function
showError(socket, new Error("This is a test error from the server!"));
if (prompt === "error") {
return {
code: "custom_error",
message: "This is a custom error",
data: { a: 1 },
} as RpcError;
} else if (prompt === "throw") {
throw new Error("This is a thrown error");
}
return `Server received: ${prompt}`;
}
);
});
httpServer.listen(8080, () => {
console.log("Server running on http://localhost:8080");
});
Use the generated functions to call server methods and handle server-initiated calls.
// pkg/client/index.ts
import { io } from "socket.io-client";
import {
generateText,
handleShowError,
handleAskQuestion,
} from "@socket-rpc/rpc/client.generated";
import { isRpcError } from "@socket-rpc/rpc";
const socket = io("http://localhost:8080");
// --- Best Practice: Đăng ký các trình xử lý sự kiện MỘT LẦN ở đây ---
// Xử lý RPC `showError` từ server
handleShowError(socket, async (error: Error): Promise<void> => {
console.error("Server sent an error:", error.message);
});
// Xử lý RPC `askQuestion` từ server (và gửi trả lời)
handleAskQuestion(socket, async (question: string) => {
console.log(`Server asked: ${question}`);
return "blue"; // Trả lời câu hỏi của server
});
// --- Logic chạy mỗi khi kết nối thành công (hoặc kết nối lại) sẽ nằm trong này ---
socket.on("connect", async () => {
console.log("Connected to the server!");
// Gọi hàm `generateText` trên server
const response = await generateText(socket, "Hello, server!", 10000); // 10s timeout
if (isRpcError(response)) {
console.error("RPC Error:", response);
} else {
console.log("Server responded:", response);
}
});
socket.on("disconnect", (reason) => {
console.log(`Disconnected from server: ${reason}`);
});
Generates the RPC code from interface definitions.
Usage:
socketrpc-gen <path> [options]
Arguments:
<path>
: Path to the input TypeScript file containing interface definitions. (Required)
Options:
-p, --package-name <name>
: The npm package name for the generated RPC code. (Default: "@socket-rpc/rpc")-t, --timeout <ms>
: Default timeout in milliseconds for RPC calls that expect a response. This can be overridden per-call. (Default: "5000")-w, --watch
: Watch for changes in the definition file and regenerate automatically. (Default: false)-h, --help
: Display help for command.
The socket-rpc
tool works by parsing your TypeScript interface file and generating a set of functions and handlers that wrap the socket.io
communication layer.
- For each function in your
ServerFunctions
interface, it generates:- A
handle<FunctionName>
function for the server to process incoming requests. - A
<functionName>
function for the client to call the server method.
- A
- For each function in your
ClientFunctions
interface, it generates:- A
handle<FunctionName>
function for the client to process incoming requests from the server. - A
<functionName>
function for the server to call the client method.
- A
This approach provides a clean and type-safe way to communicate between your client and server, without having to write any boilerplate socket.io
code yourself. It automatically handles acknowledgments for functions that return values and uses fire-and-forget for void
functions.