Rage-FW-RPC
is an all-in package with asynchronous RPC implementation for RageMP servers in JS/TS
Installation
npm i rage-fw-rpc
pnpm i rage-fw-rpc
yarn add rage-fw-rpc
Import installed package and initialize rpc:
// lib/rpc.js
import { Rpc } from 'rage-fw-rpc'
export const rpc = new Rpc(/* options */)
On client-side you have to also specify the browser you want to refer to for events
// client/index.js
import { Rpc } from 'rage-fw-rpc'
export const rpc = new Rpc(/* options */)
rpc.browser = mp.browsers.new('package://.../index.html')
Also see Rpc Config
Motivation
The idea was to create an extensible package, with various features to simplify the development process and provide as much comfort as possible. It should also be using similar architecture as the framework it was specially built for
Inspired by usage of rage-rpc
TL;DR
- Type-safe events via TS generics, avoiding type wrappers
- Built-in logging options for each environment
- Error-safe developer mode for browser
- Calls can receive response from called environments via Promises (browser -> server -> browser, etc.)
- Actual human-readable errors
Points
Before reading docs you should at least barely be acknowledged about the patterns used and how rage-fw-rpc
is different in usage than community favorite rage-rpc
- Arguments when calling any event must be wrapped in an array. This is done for proper argument typing. Be aware that as of now arguments in
register
are SPREAD and not wrapped in array. It is recommended to at least loosely type events, that require passing arrays as arguments to avoid unwanted outcomes.
// client-side
rpc.callServer('event', [2, 3])
// server-side expects number[], but gets two separate numbers as arguments instead
rpc.register('event', (player, argument1: number[] /* (actual: number), argument2 (actual: number) */) => {})
// this will save the day
rpc.callServer<[number[]]>('event', [2, 3]) // type-error
rpc.callServer<[number[]]>('event', [[2, 3]]) // ✓
- Keep in mind that chaining events this way -
// eg. client called server
rpc.register('customServerEvent', async (args: string) => {
const response: string = await rpc.callBrowser('customCefEvent', [
'hello from server',
])
// do something with response
return 'response from server'
})
has two major issues:
- Events are marked as timed out after 10s and are throwing if no response was received. This can occur when player has high ping or browser delays the response in any way. Means your event chain may not continue in those cases. Your codebase is entirely up to you, but avoid such issues try not to chain events inside one another
- await stops function execution. This means any operations in place of
do something with res
that do not rely on response are unnecessarily stopped. This can be avoided using.then(response => ...)
giving function an opportunity to execute side-operations in parallel with promises Ref: await docs Ref: in detail
- Every environment can call any environment and get a response via Promise
server <-> client <-> browser as well as server <-> browser
You can still use call<env>
methods to refer to environment itself
// server
rpc.callServer(...)
// client
rpc.callClient(...)
// browser
rpc.callBrowser(...)
It will behave the same way as using call
by just being redirected to it. For better code clearance we do recommend using call
instead of call<env>
to also same a tiny bit of computation resources
- (Extra) Due to async-based logic some IDEs or tools like ESlint can soft-warn you for ignoring promises on events you do not want response from. In that cases you can either
// await it, but you will have to mark parent function as async
await rpc.callServer('event')
// or use .then() to just remove the noisy underline in rare cases you do not want/unable to mark it as async (eg. React.useEffect)
rpc.callServer('event').then()
Docs
Rpc Config
These are the options you can specify when creating Rpc instance. Options can be omitted at all if you want so. All options only have effect in current context and have to be specified individually on server/client/browser
interface RpcWrapperConfig {
forceBrowserDevMode?: boolean // defaults to false
debugLogs?: boolean // defaults to false
}
forceBrowserDevMode
- only has effect on browser-side. Fallback for browser to launch without mp context and without errors (eg. for development in browser). Keep in mind that using this makes browser-side unavailable at all preventing from all operations except logging. Therefore is recommended to use in pair with debugLogs
debugLogs
- enables logging all exposed methods to available console. Server/browser: console.log
; client: mp.console.logInfo
// example
import { Rpc } from 'rage-fw-rpc'
export const rpc = new Rpc({
forceBrowserDevMode: false
debugLogs: true
})
register
Registers a callback function for a specified event
register<
CallbackArguments extends unknown[] = unknown[],
CallbackReturn extends unknown = unknown,
EventName extends string = string,
>(
eventName: EventName,
cb: (...args: CallbackArguments) => CallbackReturn,
): void
Example
rpc.register('playerJoin', (player) => {
console.log(`Connected: ${player.socialClub}`)
})
unregister
Cancels callback function for a specified event
unregister<EventName extends string = string>(
eventName: EventName,
): void
Example
rpc.unregister('playerDamage')
callClient
Calls a client-side event from server or browser (or client, but better use call)
async callClient<
Arguments extends unknown[] = unknown[],
EventName extends string = string,
Return extends unknown = unknown,
>(eventName: EventName, args?: Arguments): Promise<Return>
Example from browser:
rpc.callClient('updatePlayerData', ['argument']).then(response => {
console.log(`Received: ${response}`)
})
Example from server (requires player):
rpc.callClient(player, 'updatePlayerData', ['argument']).then(response => {
console.log(`Received: ${response}`)
})
callServer
Calls a server-side event from browser or client (or server, but better use call)
async callServer<
Arguments extends unknown[] = unknown[],
EventName extends string = string,
Return extends unknown = unknown,
>(eventName: EventName, args?: Arguments): Promise<Return>
Example
rpc.callServer('updatePlayerData', ['argument']).then(response => {
console.log(`Received: ${response}`)
})
callBrowser
Calls a browser-side event from server or client (or browser, but better use call)
async callBrowser<
Arguments extends unknown[] = unknown[],
EventName extends string = string,
Return extends unknown = unknown,
>(eventName: EventName, args?: Arguments): Promise<Return>
Example from client:
rpc.callBrowser('updatePlayerData', ['argument']).then(response => {
console.log(`Received: ${response}`)
})
Example from server (requires player):
rpc.callBrowser(player, 'updatePlayerData', ['argument']).then(response => {
console.log(`Received: ${response}`)
})
call
Calls an event in current environment
async call<
Arguments extends unknown[] = unknown[],
EventName extends string = string,
Return extends unknown = unknown,
>(eventName: EventName, args?: Arguments): Promise<Return>
Example
rpc.call('triggerSomething').then(response => {
console.log(`Received: ${response}`)
})
Errors
- When error is thrown you will get a message of such form
`${rpcData.knownError}\n` + // error message
`Caller: ${rpcData.calledFrom}\n` + // server/client/browser
`Receiver: ${this.environment_}\n` + // server/client/browser
`Event: ${rpcData.eventName}\n` +
`Additional Info: ${error}` // actual error object, could be more than one
Hopefully this will give you enough information about the event which throws. Error information is definitely subject to change in future
- Keep in mind that event timeouts are throwing with almost no information due to their current implementation (at least your console can show line number)
- Under the hood we use
JSON.stringify
to pass data between environments, but in JS there are a few which cannot be serialized
Codes
These should be clear enough themselves, in other cases refer to here
EVENT_NOT_REGISTERED
- throws in Promise (rejects) in called environment when event is either already unregistered or not registered yet. If you see this its almost always calling an event before registeringUNKNOWN_ENVIRONMENT
- throws in any environment that is not recognized as server/client/browser in Rage. Unlikerage-rpc
this is not thrown in browser when launched without mp context IF you specify it in browser Rpc ConfigNO_BROWSER
- throws on client if you failed to specify valid browser for it to refer to when calling browserEVENT_RESPONSE_TIMEOUT
- throws in Promise (rejects) when failed to receive a response data from called environment. You may not always want to receive it at all, for now it just works like this. Prefer addingcatch
on your events
Plans (todo)
Estimate plans for release
- Implement all calls
- Implement responses between environments
- Ability to run in browser without mp context
- Add Rpc to CLI
- Register multiple events at once
- Implement Batch call multiple events
- Opt out of event response to avoid error handling
- Separate browser dev environment (currently uses unknown)
- Improve error handling
License
MIT License
Copyright (c) 2024 Entity Seven Group