fix(rpc): improve handlers

This commit is contained in:
2026-09-28 02:05:40 +01:00
parent 558f04567a
commit b48d3f5240
6 changed files with 74 additions and 71 deletions
+2 -10
View File
@@ -13,7 +13,6 @@ import {
type RPCState,
} from '../utils/types'
/** Shared plumbing of the server, client and webview instances */
export class RPCInstanceBase {
protected readonly env: RPCEnvironment
protected readonly debug: boolean
@@ -26,9 +25,7 @@ export class RPCInstanceBase {
this._pending = new Pending(cfg.timeout ?? 5000)
}
// ===== LISTENERS =====
/** Registers `cb` for `event` on `emitter`; `method` is only used for logs */
/** Registers `cb` for `event` on `emitter`; `method` is only for logs */
protected listen(
emitter: Emitter,
method: string,
@@ -40,14 +37,13 @@ export class RPCInstanceBase {
return this
}
/** Unregisters `cb` for `event` on `emitter`; `method` is only for logs */
protected unlisten(emitter: Emitter, method: string, event: string): this {
this.log(`${method} ${event}`)
emitter.off(event)
return this
}
// ===== OUTGOING =====
/** Builds an event payload sent from this environment to `to` */
protected request(
event: string,
@@ -89,8 +85,6 @@ export class RPCInstanceBase {
if (!found) this.logIgnored(response)
}
// ===== INCOMING =====
/**
* Runs the listener for `request` and builds the response to send back.
* Never throws: a missing listener or a thrown error ends up in `error`.
@@ -147,8 +141,6 @@ export class RPCInstanceBase {
})
}
// ===== LOGS =====
/** Debug-only log, prefixed with the environment */
protected log(message: string): void {
if (this.debug) console.log(`[RPC]:${this.env}:${message}`)
+9 -18
View File
@@ -2,7 +2,7 @@ import type * as s from '@entityseven/fivem-rpc-shared-types'
import { Emitter } from '../utils/emitter'
import { RPCError } from '../utils/errors'
import { parse, stringify, stringifyWeb } from '../utils/funcs'
import { stringify, stringifyWeb } from '../utils/funcs'
import {
NATIVE_CLIENT_EVENTS,
NATIVE_CLIENT_NETWORK_EVENTS,
@@ -35,11 +35,14 @@ export class RPCInstanceClient extends RPCInstanceBase {
RegisterNuiCallbackType(RPCEvents.LISTENER_WEB)
on(
`__cfx_nui:${RPCEvents.LISTENER_WEB}`,
async (data: RPCState, callback: (res: unknown) => void) => {
async (data: unknown, callback: (res: unknown) => void) => {
const payload = this.accept(data)
if (!payload) return callback({ status: 'invalid' })
try {
callback(await this._handleWeb(data))
callback(await this._handleWeb(payload))
} catch (e) {
callback(this.errorResponse(data, e))
callback(this.errorResponse(payload, e))
}
},
)
@@ -48,16 +51,8 @@ export class RPCInstanceClient extends RPCInstanceBase {
// ===== HANDLERS =====
private async _handleServer(payloadRaw: RPCStateRaw) {
try {
parse(payloadRaw)
} catch {
throw new RPCError(RPCErrors.INVALID_DATA, RPCErrors.INVALID_DATA)
}
const payload = parse(payloadRaw)
this.log(
`accepted ${payload.type} ${payload.event} from ${payload.calledFrom}`,
)
const payload = this.accept(payloadRaw)
if (!payload) return
if (payload.type === 'event') {
if (payload.calledTo === 'client') {
@@ -86,10 +81,6 @@ export class RPCInstanceClient extends RPCInstanceBase {
}
private async _handleWeb(payload: RPCState): Promise<unknown> {
this.log(
`accepted ${payload.type} ${payload.event} from ${payload.calledFrom}`,
)
if (payload.type === 'event') {
if (payload.calledTo === 'client') {
return this.dispatch(this._emitterWeb, payload)
+5 -21
View File
@@ -2,7 +2,7 @@ import type * as s from '@entityseven/fivem-rpc-shared-types'
import { Emitter } from '../utils/emitter'
import { RPCError } from '../utils/errors'
import { parse, stringify } from '../utils/funcs'
import { stringify } from '../utils/funcs'
import { NATIVE_SERVER_EVENTS } from '../utils/native'
import {
type RPCConfig,
@@ -32,16 +32,8 @@ export class RPCInstanceServer extends RPCInstanceBase {
// ===== HANDLERS =====
private async _handleClient(payloadRaw: RPCStateRaw) {
try {
parse(payloadRaw)
} catch {
throw new RPCError(RPCErrors.INVALID_DATA, RPCErrors.INVALID_DATA)
}
const payload = parse(payloadRaw)
this.log(
`accepted ${payload.type} ${payload.event} from ${payload.calledFrom}`,
)
const payload = this.accept(payloadRaw)
if (!payload) return
if (payload.calledFrom === 'client') {
if (payload.type === 'event') {
@@ -71,16 +63,8 @@ export class RPCInstanceServer extends RPCInstanceBase {
}
private async _handleWeb(payloadRaw: RPCStateRaw) {
try {
parse(payloadRaw)
} catch {
throw new RPCError(RPCErrors.INVALID_DATA, RPCErrors.INVALID_DATA)
}
const payload = parse(payloadRaw)
this.log(
`accepted ${payload.type} ${payload.event} from ${payload.calledFrom}`,
)
const payload = this.accept(payloadRaw)
if (!payload) return
if (payload.calledFrom === 'webview') {
if (payload.type === 'event') {
+22 -16
View File
@@ -23,23 +23,30 @@ export class RPCInstanceWebview extends RPCInstanceBase {
console.log('[RPC] Initialized Webview')
window.addEventListener('message', (e: MessageEvent<RPCStateWeb>) => {
if (e.data.origin === RPCEvents.LISTENER_CLIENT) {
this._handleClient(e.data.data)
window.addEventListener(
'message',
(e: MessageEvent<Partial<RPCStateWeb> | null>) => {
const origin = e.data?.origin
// not ours, e.g. the resource's own SendNUIMessage calls
if (
origin !== RPCEvents.LISTENER_CLIENT &&
origin !== RPCEvents.LISTENER_SERVER
) {
return
}
if (e.data.origin === RPCEvents.LISTENER_SERVER) {
this._handleServer(e.data.data)
}
})
const payload = this.accept(e.data?.data)
if (!payload) return
if (origin === RPCEvents.LISTENER_CLIENT) this._handleClient(payload)
else this._handleServer(payload)
},
)
}
// ===== HANDLERS =====
private async _handleClient(payload: RPCState) {
this.log(
`accepted ${payload.type} ${payload.event} from ${payload.calledFrom}`,
)
if (payload.calledFrom === 'client' && payload.type === 'event') {
const response = await this.dispatch(this._emitterClient, payload)
@@ -48,10 +55,6 @@ export class RPCInstanceWebview extends RPCInstanceBase {
}
private async _handleServer(payload: RPCState) {
this.log(
`accepted ${payload.type} ${payload.event} from ${payload.calledFrom}`,
)
if (payload.calledFrom === 'server' && payload.type === 'event') {
const response = await this.dispatch(this._emitterServer, payload)
@@ -156,7 +159,10 @@ export class RPCInstanceWebview extends RPCInstanceBase {
private _request<R>(payload: RPCState): Promise<R> {
const response = this._pending.wait<R>(payload)
this._createHttpClientRequest<RPCState>(payload).then(
res => this.settle(res),
res => {
const reply = this.accept(res)
if (reply) this.settle(reply)
},
(error: Error) => this._pending.reject(payload.uuid, error),
)
return response
+34 -3
View File
@@ -1,17 +1,48 @@
import type {
RPCEnvironment,
RPCState,
RPCStateRaw,
RPCStateWeb,
RPCStateWebRaw,
} from './types'
const ENVIRONMENTS: readonly unknown[] = [
'server',
'client',
'webview',
] satisfies RPCEnvironment[]
/**
* **Internal**
*
* Typed data parser
* Checks the shape of an incoming payload. Payloads come from the network or
* another runtime, so nothing about them is trusted.
*/
export function parse(data: RPCStateRaw): RPCState {
return JSON.parse(data)
export function isRPCState(value: unknown): value is RPCState {
if (typeof value !== 'object' || value === null) return false
const v = value as Record<string, unknown>
return (
typeof v.event === 'string' &&
typeof v.uuid === 'string' &&
(v.type === 'event' || v.type === 'response') &&
ENVIRONMENTS.includes(v.calledFrom) &&
ENVIRONMENTS.includes(v.calledTo) &&
(v.data === null || Array.isArray(v.data))
)
}
/**
* **Internal**
*
* Parses a raw payload, `null` if it is not JSON or not an RPC payload
*/
export function parse(data: RPCStateRaw): RPCState | null {
try {
const value: unknown = JSON.parse(data)
return isRPCState(value) ? value : null
} catch {
return null
}
}
/**
-1
View File
@@ -94,7 +94,6 @@ export enum RPCEvents {
*/
export enum RPCErrors {
EVENT_NOT_REGISTERED = 'Event not registered',
INVALID_DATA = 'Invalid data (possibly broken JSON)',
NO_PLAYER = 'No player (failed to resolve from local index)',
UNKNOWN_NATIVE = 'Unknown native event (if you are sure this exists - use native handler)',
UNKNOWN_ENVIRONMENT = 'Unknown environment (must be either "server", "client" or "webview")',