Skip to content

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.

bash
npm install @celljs/rpc
ts
// 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 ​

WhereHow
A React component of the same applicationuseRpc<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 HTTPRpcUtil.create<T>('https://host/api', Id)

The endpoint of a service is resolved from:

yaml
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 ​

yaml
rpc:
  transport: websocket          # default http

Proxies 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:

ts
// 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.

KeyDefault
rpc.websocket.allowedOriginsOrigins 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.maxPayload1048576Largest message in bytes; a larger one closes the connection (1009)
rpc.websocket.heartbeat30000Milliseconds between pings of the server; a client that answers none is dropped
rpc.websocket.callTimeout30000Milliseconds a call waits for its answer
rpc.websocket.reconnectDelay1000Milliseconds before a client tries again; doubles up to 30 s
rpc.websocket.endpointthe page's originws:// / 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.