@vicerp/rpc
v1.0.0
Published
Platform-agnostic bidirectional RPC library for game client <-> browser communication
Readme
@vicerp/rpc
Platform-agnostic bidirectional RPC library for game frameworks. Provides procedure calls, events, and relay capabilities across the full Browser <-> Client <-> Server chain.
Architecture
Browser ──RPC──> Client ──RPC──> Server ──gRPC──> Engine
Browser <──RPC── Client <──RPC── Server <──gRPC── EngineThe client acts as a pure passthrough relay. All RPC messages from the browser are forwarded to the server and vice versa. The server is where business logic lives.
Installation
npm install @vicerp/rpcCore API
Rpc
The main class for registering procedures, making calls, and sending events.
import { Rpc } from '@vicerp/rpc';
const rpc = new Rpc(transport);
// Register a procedure
rpc.register('inventory:split', async (args) => {
const { slot } = args as { slot: number };
// ... business logic
return { success: true };
});
// Call a remote procedure
const result = await rpc.call<{ success: boolean }>('inventory:split', { slot: 4 });
// Fire-and-forget event
rpc.trigger('inventory:update', { items: [...] });
// Listen for events
rpc.on('inventory:update', (args) => {
console.log('Inventory updated:', args);
});
// Unsubscribe
rpc.off('inventory:update', handler);
// Cleanup
rpc.destroy();Options
// Call with custom timeout (default: 10s)
const result = await rpc.call('procedure', args, { timeout: 5000 });RpcRelay
Bidirectionally forwards all messages between two transports. Zero knowledge of message content — just pipes through.
import { RpcRelay } from '@vicerp/rpc';
const relay = new RpcRelay(transportA, transportB);
// Messages from A are forwarded to B, and vice versa
// Cleanup
relay.destroy();RpcTransport (interface)
All transports implement this interface:
interface RpcTransport {
send(message: RpcMessage): void;
onMessage(handler: (message: RpcMessage) => void): void;
destroy(): void;
}Message Types
// Procedure call
interface RpcRequest {
type: 'request';
id: string;
procedure: string;
args?: unknown;
}
// Procedure response
interface RpcResponse {
type: 'response';
id: string;
result?: unknown;
error?: string;
}
// Fire-and-forget event
interface RpcEvent {
type: 'event';
name: string;
args?: unknown;
}Adapters
RAGE:MP
Browser (CEF) Transport
import { RageBrowserTransport } from '@vicerp/rpc/rage/browser';
// In CEF browser code
const transport = new RageBrowserTransport(mp);
const rpc = new Rpc(transport);Client Transport (Client <-> Browser)
import { RageClientTransport } from '@vicerp/rpc/rage/client';
// In client script
const transport = new RageClientTransport(browser, mp);
const rpc = new Rpc(transport);Client-Server Transport (Client <-> Server)
import { RageClientServerTransport } from '@vicerp/rpc/rage/client-server';
// In client script — sends via mp.events.callRemote(), receives via mp.events
const transport = new RageClientServerTransport(mp);Server Transport (Server <-> Client)
import { RageServerTransportHub, RageServerTransport } from '@vicerp/rpc/rage/server';
// Create ONE hub per server (registers global event listener)
const hub = new RageServerTransportHub(mp);
// Create per-player transports
const transport = hub.createTransport(player);
const rpc = new Rpc(transport);
// Cleanup when player disconnects
hub.removeTransport(player);
// Shutdown
hub.destroy();Client as Relay (recommended pattern)
The client doesn't need its own Rpc instance — it just relays between browser and server:
import { RpcRelay } from '@vicerp/rpc';
import { RageClientTransport } from '@vicerp/rpc/rage/client';
import { RageClientServerTransport } from '@vicerp/rpc/rage/client-server';
const browserTransport = new RageClientTransport(browser, mp);
const serverTransport = new RageClientServerTransport(mp);
const relay = new RpcRelay(browserTransport, serverTransport);FiveM
NUI Transport (Browser side)
import { FiveMNuiTransport } from '@vicerp/rpc/fivem/nui';
const transport = new FiveMNuiTransport(window);
const rpc = new Rpc(transport);Client Transport (Client <-> NUI)
import { FiveMClientTransport } from '@vicerp/rpc/fivem/client';
const transport = new FiveMClientTransport(fivem);
const rpc = new Rpc(transport);Client-Server Transport (Client <-> Server)
import { FiveMClientServerTransport } from '@vicerp/rpc/fivem/client-server';
// Sends via TriggerServerEvent(), receives via on()
const transport = new FiveMClientServerTransport(fivem);Server Transport (Server <-> Client)
import { FiveMServerTransportHub } from '@vicerp/rpc/fivem/server';
// Create ONE hub per server (registers global onNet listener)
const hub = new FiveMServerTransportHub(fivem);
// Create per-player transports (uses source for player ID)
const transport = hub.createTransport(playerId);
const rpc = new Rpc(transport);
// Cleanup
hub.removeTransport(playerId);
hub.destroy();React Integration
import { RpcProvider, useRpc, useRpcCall, useRpcEvent, useRpcRegister } from '@vicerp/rpc/react';RpcProvider
Wraps your app and provides an Rpc instance via context.
<RpcProvider transport={transport}>
<App />
</RpcProvider>useRpc()
Access the Rpc instance directly.
const rpc = useRpc();useRpcCall<T>(procedure, options?)
Make RPC calls with loading/error state management.
const { data, loading, error, call } = useRpcCall<Item[]>('inventory:getItems');
// Trigger the call
await call();
// Or with args
await call({ playerId: '123' });useRpcEvent(name, handler)
Subscribe to events (auto-cleanup on unmount).
useRpcEvent('inventory:update', (items) => {
setInventory(items);
});useRpcRegister(name, handler)
Register a procedure handler (auto-cleanup on unmount).
useRpcRegister('ui:getState', () => {
return { theme: 'dark', locale: 'en' };
});Full Chain Example
Player clicks "split" button in inventory UI:
1. Browser: rpc.call('inventory:split', { slot: 4 })
2. Client: RpcRelay forwards to server
3. Server: Rpc handler receives, sends gRPC to engine
4. Engine: Processes split, responds via gRPC
5. Server: rpc.trigger('inventory:update', { ... })
6. Client: RpcRelay forwards to browser
7. Browser: useRpcEvent('inventory:update', ...) firesBrowser (React)
import { RpcProvider, useRpcCall, useRpcEvent } from '@vicerp/rpc/react';
import { RageBrowserTransport } from '@vicerp/rpc/rage/browser';
const transport = new RageBrowserTransport(mp);
function Inventory() {
const [items, setItems] = useState([]);
const { call: splitItem } = useRpcCall('inventory:split');
useRpcEvent('inventory:update', (data) => setItems(data.items));
return (
<button onClick={() => splitItem({ slot: 4 })}>Split</button>
);
}
function App() {
return (
<RpcProvider transport={transport}>
<Inventory />
</RpcProvider>
);
}Client (RAGE:MP)
import { RpcRelay } from '@vicerp/rpc';
import { RageClientTransport } from '@vicerp/rpc/rage/client';
import { RageClientServerTransport } from '@vicerp/rpc/rage/client-server';
const browser = mp.browsers.new('http://localhost:3005');
const browserTransport = new RageClientTransport(browser, mp);
const serverTransport = new RageClientServerTransport(mp);
const relay = new RpcRelay(browserTransport, serverTransport);Server (RAGE:MP)
import { Rpc } from '@vicerp/rpc';
import { RageServerTransportHub } from '@vicerp/rpc/rage/server';
const hub = new RageServerTransportHub(mp);
mp.events.add('playerReady', (player) => {
const transport = hub.createTransport(player);
const rpc = new Rpc(transport);
rpc.register('inventory:split', async (args) => {
const { slot } = args as { slot: number };
// Call engine via gRPC...
const result = await engineConnection.splitItem(playerId, slot);
// Push update back to browser
rpc.trigger('inventory:update', { items: result.items });
return { success: true };
});
});Subpath Exports
| Import path | Description |
|---|---|
| @vicerp/rpc | Core: Rpc, RpcRelay, types, errors |
| @vicerp/rpc/rage/browser | RAGE:MP CEF browser transport |
| @vicerp/rpc/rage/client | RAGE:MP client transport (client <-> browser) |
| @vicerp/rpc/rage/client-server | RAGE:MP client transport (client <-> server) |
| @vicerp/rpc/rage/server | RAGE:MP server transport hub + per-player transport |
| @vicerp/rpc/fivem/nui | FiveM NUI browser transport |
| @vicerp/rpc/fivem/client | FiveM client transport (client <-> NUI) |
| @vicerp/rpc/fivem/client-server | FiveM client transport (client <-> server) |
| @vicerp/rpc/fivem/server | FiveM server transport hub + per-player transport |
| @vicerp/rpc/react | React hooks and provider |
Development
# Install dependencies
npm install
# Run tests
npm test
# Run tests in watch mode
npm run test:watch
# Run tests with coverage
npm run test:cov
# Build (CJS + ESM)
npm run build
# Clean build output
npm run cleanPublishing
# From libs/vice/rpc directory:
npm publish --access publicThe prepublishOnly script automatically runs clean, build, and test before publishing.
License
MIT
