1 RPC@0.2.5
rilaxik edited this page 2024-10-28 13:18:27 +00:00

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:

  1. 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
  2. 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 registering
  • UNKNOWN_ENVIRONMENT - throws in any environment that is not recognized as server/client/browser in Rage. Unlike rage-rpc this is not thrown in browser when launched without mp context IF you specify it in browser Rpc Config
  • NO_BROWSER - throws on client if you failed to specify valid browser for it to refer to when calling browser
  • EVENT_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 adding catch 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