Cell - Rpc Component
Typed remote calls between the frontend and the backend of a Cell application, or between Cell backends: JSON-RPC 2.0 over HTTP or over one WebSocket connection, with the same TypeScript interface on both sides. Over WebSocket the backend can also call the frontend. Guide: Frontend and RPC.
npm install @celljs/rpc// src/common/greeting-protocol.ts
export const GreetingServer = Symbol('GreetingServer');
export interface GreetingServer {
greet(name: string): Promise<string>;
}
// src/node/greeting-server.ts
import { Rpc } from '@celljs/rpc';
@Rpc(GreetingServer)
export class GreetingServerImpl implements GreetingServer {
async greet(name: string) { return `Hello, ${name}`; }
}
// frontend (a React component)
import { useRpc } from '@celljs/react';
const greetingServer = useRpc<GreetingServer>(GreetingServer);
const text = await greetingServer.greet('web');Services
@Rpc(id) registers the class as a component (it takes ComponentOptions like @Component) and serves it at POST /<rpc.path>/<id>, by default /api/GreetingServer (rpc.path: api, under server.path). The body is a JSON-RPC request ({ "jsonrpc": "2.0", "id": 1, "method": "greet", "params": ["web"] }), a batch (an array), or a notification (no id, answered with 204). Only methods declared by the class (and its superclasses) can be called, not constructor or those of Object. Parameters typed with a class are converted and validated by @celljs/validation when it is installed.
Errors answer as HTTP errors do: an ApplicationError keeps its code, message and data; a 4xx HttpError its message; any other error only Internal Server Error in production (web.errors.exposeMessages), and it is logged. Stacks never leave the server. The caller gets an RpcError with code (JSON-RPC), status (HTTP) and a stack that leads to the call, or the ApplicationError again.
Clients
| Where | How |
|---|---|
| A React component of the same application | useRpc<T>(Id) of @celljs/react: the endpoint is the page's origin |
| A component (frontend or backend) | @Autorpc(Id) protected readonly service: T |
| Anywhere, with an explicit URL, over HTTP | RpcUtil.create<T>('https://host/api', Id) |
The endpoint of a service is resolved from:
rpc:
endpoint: # per service id; always HTTP
GreetingServer: "http://greetings:3000/api/GreetingServer"
defaultEndpoint: "http://gateway:3000{rpcPath}/{serviceIdentifier}"{rpcPath} and {serviceIdentifier} are replaced. Without either, a browser uses location; a backend without them fails with EndpointNotFoundError.
WebSocket
rpc:
transport: websocket # default httpProxies then call the services without their own rpc.endpoint over one WebSocket connection per client, at /<rpc.path>/ws (methods are <service>/<method>). Each call passes the middlewares like an HTTP call, with the headers and cookies of the connection's upgrade request, so sessions and @celljs/security apply. While no connection can be made (an old browser, a platform without WebSocket support such as the serverless presets, a proxy that drops upgrades) calls go over HTTP, and the client tries again later; a connection that drops answers its pending calls with RpcError code -32003 (they are not sent again: they may have run).
The backend calls the frontend through the connection of a call, or any open connection:
// src/common/notice-protocol.ts
export const NoticeClient = Symbol('NoticeClient');
export interface NoticeClient {
notify(text: string): Promise<void>;
}
// src/browser/notice-client.ts (imported by src/browser/module.ts)
import { RpcClient } from '@celljs/rpc';
@RpcClient(NoticeClient)
export class NoticeClientImpl implements NoticeClient {
async notify(text: string) { /* show it */ }
}
// src/node/order-server.ts
import { Autowired } from '@celljs/core';
import { Rpc } from '@celljs/rpc';
import { RpcConnectionManager, RpcContext } from '@celljs/rpc/lib/node';
@Rpc(OrderServer)
export class OrderServerImpl implements OrderServer {
@Autowired(RpcConnectionManager)
protected readonly connections: RpcConnectionManager;
async watch(): Promise<void> {
const connection = RpcContext.requireConnection(); // the caller's connection
connection.onClose(() => { /* stop watching */ });
await connection.client<NoticeClient>(NoticeClient).notify('watching');
}
async announce(text: string): Promise<void> {
await Promise.all(this.connections.connections.map(c => c.client<NoticeClient>(NoticeClient).notify(text)));
}
}With @RpcClient components the frontend opens the connection as it starts and reopens it when it drops. RpcContext.connection() is undefined for a call over HTTP; requireConnection() then throws RpcNoConnectionError (400, CELL_RPC_NO_CONNECTION). Calls to a client time out after rpc.websocket.callTimeout; a closed connection rejects them.
| Key | Default | |
|---|---|---|
rpc.websocket.allowedOrigins | Origins besides the server's own whose pages may connect, or *. Browsers send the cookies of the server with a WebSocket request from any page, so other origins are refused (403) | |
rpc.websocket.maxPayload | 1048576 | Largest message in bytes; a larger one closes the connection (1009) |
rpc.websocket.heartbeat | 30000 | Milliseconds between pings of the server; a client that answers none is dropped |
rpc.websocket.callTimeout | 30000 | Milliseconds a call waits for its answer |
rpc.websocket.reconnectDelay | 1000 | Milliseconds before a client tries again; doubles up to 30 s |
rpc.websocket.endpoint | the page's origin | ws:// / wss:// URL to connect to; needed by a backend calling another backend over WebSocket |
The server side is part of the package (no dependency); it needs a runtime whose node:http hands over upgrades: Node.js, Deno, and Bun 1.4 or later (older Bun versions log a warning and the clients use HTTP).
A RpcWebSocketGuard component (allows(request): boolean) decides on the upgrade request whether a connection may open, before anything runs over it; refused connections get a 401. All guards must agree.
Batching
Over HTTP, rpc.merge.enabled: true makes the proxies send the calls made within timerDelay (35 ms) in one request (a JSON-RPC batch), up to maxCount calls or maxLength bytes.