mirror of
https://github.com/rilaxik/fivem-rpc.git
synced 2026-09-28 01:29:13 +00:00
fix(rpc): better error handling
This commit is contained in:
+24
-33
@@ -1,6 +1,7 @@
|
|||||||
import type * as s from '@entityseven/fivem-rpc-shared-types'
|
import type * as s from '@entityseven/fivem-rpc-shared-types'
|
||||||
|
|
||||||
import { Emitter } from '../utils/emitter'
|
import { Emitter } from '../utils/emitter'
|
||||||
|
import { RPCError } from '../utils/errors'
|
||||||
import { generateUUID, parse, stringify, stringifyWeb } from '../utils/funcs'
|
import { generateUUID, parse, stringify, stringifyWeb } from '../utils/funcs'
|
||||||
import {
|
import {
|
||||||
NATIVE_CLIENT_EVENTS,
|
NATIVE_CLIENT_EVENTS,
|
||||||
@@ -35,8 +36,11 @@ export class RPCInstanceClient extends Wrapper {
|
|||||||
on(
|
on(
|
||||||
`__cfx_nui:${RPCEvents.LISTENER_WEB}`,
|
`__cfx_nui:${RPCEvents.LISTENER_WEB}`,
|
||||||
async (data: RPCState, callback: (res: unknown) => void) => {
|
async (data: RPCState, callback: (res: unknown) => void) => {
|
||||||
const res = await this._handleWeb(data)
|
try {
|
||||||
callback(res)
|
callback(await this._handleWeb(data))
|
||||||
|
} catch (e) {
|
||||||
|
callback(this.errorResponse(data, e))
|
||||||
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -47,7 +51,7 @@ export class RPCInstanceClient extends Wrapper {
|
|||||||
try {
|
try {
|
||||||
parse(payloadRaw)
|
parse(payloadRaw)
|
||||||
} catch {
|
} catch {
|
||||||
throw new Error(RPCErrors.INVALID_DATA)
|
throw new RPCError(RPCErrors.INVALID_DATA, RPCErrors.INVALID_DATA)
|
||||||
}
|
}
|
||||||
const payload = parse(payloadRaw)
|
const payload = parse(payloadRaw)
|
||||||
|
|
||||||
@@ -59,23 +63,7 @@ export class RPCInstanceClient extends Wrapper {
|
|||||||
|
|
||||||
if (payload.type === 'event') {
|
if (payload.type === 'event') {
|
||||||
if (payload.calledTo === 'client') {
|
if (payload.calledTo === 'client') {
|
||||||
this.verifyEvent(this._emitterServer, payload)
|
const response = await this.dispatch(this._emitterServer, payload)
|
||||||
|
|
||||||
const responseData = await this._emitterServer.emit(
|
|
||||||
payload.event,
|
|
||||||
...(payload.data && payload.data.length > 0 ? payload.data : []),
|
|
||||||
)
|
|
||||||
|
|
||||||
const response: RPCState = {
|
|
||||||
event: payload.event,
|
|
||||||
uuid: payload.uuid,
|
|
||||||
calledFrom: 'client',
|
|
||||||
calledTo: 'server',
|
|
||||||
error: null,
|
|
||||||
data: [responseData],
|
|
||||||
player: payload.player,
|
|
||||||
type: 'response',
|
|
||||||
}
|
|
||||||
|
|
||||||
emitNet(RPCEvents.LISTENER_CLIENT, stringify(response))
|
emitNet(RPCEvents.LISTENER_CLIENT, stringify(response))
|
||||||
}
|
}
|
||||||
@@ -87,8 +75,14 @@ export class RPCInstanceClient extends Wrapper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (payload.type === 'response') {
|
if (payload.type === 'response') {
|
||||||
if (payload.calledTo === 'client' || payload.calledTo === 'webview') {
|
if (payload.calledTo === 'client') {
|
||||||
this.resolvePending(payload)
|
this.settle(payload)
|
||||||
|
}
|
||||||
|
if (payload.calledTo === 'webview') {
|
||||||
|
// relayed webview -> server call: the webview gets the whole response
|
||||||
|
if (!this._pending.resolve(payload.uuid, payload)) {
|
||||||
|
this.logIgnored(payload)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -102,22 +96,19 @@ export class RPCInstanceClient extends Wrapper {
|
|||||||
|
|
||||||
if (payload.type === 'event') {
|
if (payload.type === 'event') {
|
||||||
if (payload.calledTo === 'client') {
|
if (payload.calledTo === 'client') {
|
||||||
return await this._emitterWeb.emit(
|
return this.dispatch(this._emitterWeb, payload)
|
||||||
payload.event,
|
|
||||||
...(payload.data && payload.data.length > 0 ? payload.data : []),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
if (payload.calledTo === 'server') {
|
if (payload.calledTo === 'server') {
|
||||||
payload.player = GetPlayerServerId(PlayerId())
|
payload.player = GetPlayerServerId(PlayerId())
|
||||||
emitNet(RPCEvents.LISTENER_WEB, stringify(payload))
|
emitNet(RPCEvents.LISTENER_WEB, stringify(payload))
|
||||||
|
|
||||||
return this._pending.wait(payload.uuid)
|
return this._pending.wait<RPCState>(payload)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (payload.type === 'response') {
|
if (payload.type === 'response') {
|
||||||
if (payload.calledTo === 'client') {
|
if (payload.calledTo === 'client') {
|
||||||
this.resolvePending(payload)
|
this.settle(payload)
|
||||||
|
|
||||||
return { status: 'ok' }
|
return { status: 'ok' }
|
||||||
}
|
}
|
||||||
@@ -182,7 +173,7 @@ export class RPCInstanceClient extends Wrapper {
|
|||||||
|
|
||||||
emitNet(RPCEvents.LISTENER_CLIENT, stringify(payload))
|
emitNet(RPCEvents.LISTENER_CLIENT, stringify(payload))
|
||||||
|
|
||||||
return this._pending.wait<Awaited<Response>>(payload.uuid)
|
return this._pending.wait<Awaited<Response>>(payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== WEBVIEW =====
|
// ===== WEBVIEW =====
|
||||||
@@ -239,7 +230,7 @@ export class RPCInstanceClient extends Wrapper {
|
|||||||
data: payload,
|
data: payload,
|
||||||
})
|
})
|
||||||
|
|
||||||
return this._pending.wait<Awaited<Response>>(payload.uuid)
|
return this._pending.wait<Awaited<Response>>(payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== SELF =====
|
// ===== SELF =====
|
||||||
@@ -297,7 +288,7 @@ export class RPCInstanceClient extends Wrapper {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.verifyEvent(this._emitterLocal, payload)
|
this.assertListener(this._emitterLocal, payload.event)
|
||||||
|
|
||||||
return await this._emitterLocal.emit<Awaited<Response>>(
|
return await this._emitterLocal.emit<Awaited<Response>>(
|
||||||
payload.event,
|
payload.event,
|
||||||
@@ -328,7 +319,7 @@ export class RPCInstanceClient extends Wrapper {
|
|||||||
CallbackArguments extends Parameters<RPCNativeClientEvents[EventName]>,
|
CallbackArguments extends Parameters<RPCNativeClientEvents[EventName]>,
|
||||||
>(eventName: EventName, cb: (...args: CallbackArguments) => void): this {
|
>(eventName: EventName, cb: (...args: CallbackArguments) => void): this {
|
||||||
if (!NATIVE_CLIENT_EVENTS.includes(eventName)) {
|
if (!NATIVE_CLIENT_EVENTS.includes(eventName)) {
|
||||||
throw new Error(RPCErrors.UNKNOWN_NATIVE)
|
throw new RPCError(RPCErrors.UNKNOWN_NATIVE, RPCErrors.UNKNOWN_NATIVE)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.debug) {
|
if (this.debug) {
|
||||||
@@ -347,7 +338,7 @@ export class RPCInstanceClient extends Wrapper {
|
|||||||
>,
|
>,
|
||||||
>(eventName: EventName, cb: (...args: CallbackArguments) => void): this {
|
>(eventName: EventName, cb: (...args: CallbackArguments) => void): this {
|
||||||
if (!NATIVE_CLIENT_NETWORK_EVENTS.includes(eventName)) {
|
if (!NATIVE_CLIENT_NETWORK_EVENTS.includes(eventName)) {
|
||||||
throw new Error(RPCErrors.UNKNOWN_NATIVE)
|
throw new RPCError(RPCErrors.UNKNOWN_NATIVE, RPCErrors.UNKNOWN_NATIVE)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.debug) {
|
if (this.debug) {
|
||||||
|
|||||||
+29
-42
@@ -1,6 +1,7 @@
|
|||||||
import type * as s from '@entityseven/fivem-rpc-shared-types'
|
import type * as s from '@entityseven/fivem-rpc-shared-types'
|
||||||
|
|
||||||
import { Emitter } from '../utils/emitter'
|
import { Emitter } from '../utils/emitter'
|
||||||
|
import { RPCError } from '../utils/errors'
|
||||||
import { generateUUID, parse, stringify } from '../utils/funcs'
|
import { generateUUID, parse, stringify } from '../utils/funcs'
|
||||||
import { NATIVE_SERVER_EVENTS } from '../utils/native'
|
import { NATIVE_SERVER_EVENTS } from '../utils/native'
|
||||||
import {
|
import {
|
||||||
@@ -35,7 +36,7 @@ export class RPCInstanceServer extends Wrapper {
|
|||||||
try {
|
try {
|
||||||
parse(payloadRaw)
|
parse(payloadRaw)
|
||||||
} catch {
|
} catch {
|
||||||
throw new Error(RPCErrors.INVALID_DATA)
|
throw new RPCError(RPCErrors.INVALID_DATA, RPCErrors.INVALID_DATA)
|
||||||
}
|
}
|
||||||
const payload = parse(payloadRaw)
|
const payload = parse(payloadRaw)
|
||||||
|
|
||||||
@@ -47,34 +48,27 @@ export class RPCInstanceServer extends Wrapper {
|
|||||||
|
|
||||||
if (payload.calledFrom === 'client') {
|
if (payload.calledFrom === 'client') {
|
||||||
if (payload.type === 'event') {
|
if (payload.type === 'event') {
|
||||||
this.verifyEvent(this._emitterClient, payload)
|
|
||||||
if (payload.player === null || payload.player === -1) {
|
if (payload.player === null || payload.player === -1) {
|
||||||
payload.error = RPCErrors.NO_PLAYER
|
// nobody to reply to, the caller times out
|
||||||
this.triggerError(payload)
|
this.console.error(
|
||||||
|
new RPCError(
|
||||||
|
RPCErrors.NO_PLAYER,
|
||||||
|
`${RPCErrors.NO_PLAYER}: "${payload.event}" from ${payload.calledFrom}`,
|
||||||
|
),
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const responseData = await this._emitterClient.emit(
|
const response = await this.dispatch(
|
||||||
payload.event,
|
this._emitterClient,
|
||||||
|
payload,
|
||||||
payload.player,
|
payload.player,
|
||||||
...(payload.data && payload.data.length > 0 ? payload.data : []),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const response: RPCState = {
|
|
||||||
event: payload.event,
|
|
||||||
uuid: payload.uuid,
|
|
||||||
calledFrom: 'server',
|
|
||||||
calledTo: 'client',
|
|
||||||
error: null,
|
|
||||||
data: [responseData],
|
|
||||||
player: payload.player,
|
|
||||||
type: 'response',
|
|
||||||
}
|
|
||||||
|
|
||||||
emitNet(RPCEvents.LISTENER_SERVER, response.player, stringify(response))
|
emitNet(RPCEvents.LISTENER_SERVER, response.player, stringify(response))
|
||||||
}
|
}
|
||||||
if (payload.type === 'response') {
|
if (payload.type === 'response') {
|
||||||
this.resolvePending(payload)
|
this.settle(payload)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -83,7 +77,7 @@ export class RPCInstanceServer extends Wrapper {
|
|||||||
try {
|
try {
|
||||||
parse(payloadRaw)
|
parse(payloadRaw)
|
||||||
} catch {
|
} catch {
|
||||||
throw new Error(RPCErrors.INVALID_DATA)
|
throw new RPCError(RPCErrors.INVALID_DATA, RPCErrors.INVALID_DATA)
|
||||||
}
|
}
|
||||||
const payload = parse(payloadRaw)
|
const payload = parse(payloadRaw)
|
||||||
|
|
||||||
@@ -95,34 +89,27 @@ export class RPCInstanceServer extends Wrapper {
|
|||||||
|
|
||||||
if (payload.calledFrom === 'webview') {
|
if (payload.calledFrom === 'webview') {
|
||||||
if (payload.type === 'event') {
|
if (payload.type === 'event') {
|
||||||
this.verifyEvent(this._emitterWeb, payload)
|
|
||||||
if (payload.player === null || payload.player === -1) {
|
if (payload.player === null || payload.player === -1) {
|
||||||
payload.error = RPCErrors.NO_PLAYER
|
// nobody to reply to, the caller times out
|
||||||
this.triggerError(payload)
|
this.console.error(
|
||||||
|
new RPCError(
|
||||||
|
RPCErrors.NO_PLAYER,
|
||||||
|
`${RPCErrors.NO_PLAYER}: "${payload.event}" from ${payload.calledFrom}`,
|
||||||
|
),
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const responseData = await this._emitterWeb.emit(
|
const response = await this.dispatch(
|
||||||
payload.event,
|
this._emitterWeb,
|
||||||
|
payload,
|
||||||
payload.player,
|
payload.player,
|
||||||
...(payload.data && payload.data.length > 0 ? payload.data : []),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const response: RPCState = {
|
|
||||||
event: payload.event,
|
|
||||||
uuid: payload.uuid,
|
|
||||||
calledFrom: 'server',
|
|
||||||
calledTo: 'webview',
|
|
||||||
error: null,
|
|
||||||
data: [responseData],
|
|
||||||
player: payload.player,
|
|
||||||
type: 'response',
|
|
||||||
}
|
|
||||||
|
|
||||||
emitNet(RPCEvents.LISTENER_SERVER, response.player, stringify(response))
|
emitNet(RPCEvents.LISTENER_SERVER, response.player, stringify(response))
|
||||||
}
|
}
|
||||||
if (payload.type === 'response') {
|
if (payload.type === 'response') {
|
||||||
this.resolvePending(payload)
|
this.settle(payload)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -183,7 +170,7 @@ export class RPCInstanceServer extends Wrapper {
|
|||||||
|
|
||||||
emitNet(RPCEvents.LISTENER_SERVER, player, stringify(payload))
|
emitNet(RPCEvents.LISTENER_SERVER, player, stringify(payload))
|
||||||
|
|
||||||
return this._pending.wait<Awaited<Response>>(payload.uuid)
|
return this._pending.wait<Awaited<Response>>(payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
public async emitClientEveryone<
|
public async emitClientEveryone<
|
||||||
@@ -260,7 +247,7 @@ export class RPCInstanceServer extends Wrapper {
|
|||||||
|
|
||||||
emitNet(RPCEvents.LISTENER_SERVER, player, stringify(payload))
|
emitNet(RPCEvents.LISTENER_SERVER, player, stringify(payload))
|
||||||
|
|
||||||
return this._pending.wait<Awaited<Response>>(payload.uuid)
|
return this._pending.wait<Awaited<Response>>(payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== SELF =====
|
// ===== SELF =====
|
||||||
@@ -318,7 +305,7 @@ export class RPCInstanceServer extends Wrapper {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.verifyEvent(this._emitterLocal, payload)
|
this.assertListener(this._emitterLocal, payload.event)
|
||||||
|
|
||||||
return await this._emitterLocal.emit<Awaited<Response>>(
|
return await this._emitterLocal.emit<Awaited<Response>>(
|
||||||
payload.event,
|
payload.event,
|
||||||
@@ -350,7 +337,7 @@ export class RPCInstanceServer extends Wrapper {
|
|||||||
CallbackArguments extends Parameters<RPCNativeServerEvents[EventName]>,
|
CallbackArguments extends Parameters<RPCNativeServerEvents[EventName]>,
|
||||||
>(eventName: EventName, cb: (...args: CallbackArguments) => void): this {
|
>(eventName: EventName, cb: (...args: CallbackArguments) => void): this {
|
||||||
if (!NATIVE_SERVER_EVENTS.includes(eventName)) {
|
if (!NATIVE_SERVER_EVENTS.includes(eventName)) {
|
||||||
throw new Error(RPCErrors.UNKNOWN_NATIVE)
|
throw new RPCError(RPCErrors.UNKNOWN_NATIVE, RPCErrors.UNKNOWN_NATIVE)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.debug) {
|
if (this.debug) {
|
||||||
|
|||||||
+7
-39
@@ -43,25 +43,9 @@ export class RPCInstanceWebview extends Wrapper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (payload.calledFrom === 'client' && payload.type === 'event') {
|
if (payload.calledFrom === 'client' && payload.type === 'event') {
|
||||||
this.verifyEvent(this._emitterClient, payload)
|
const response = await this.dispatch(this._emitterClient, payload)
|
||||||
|
|
||||||
const responseData = await this._emitterClient.emit(
|
await this._createHttpClientRequest(response)
|
||||||
payload.event,
|
|
||||||
...(payload.data && payload.data.length > 0 ? payload.data : []),
|
|
||||||
)
|
|
||||||
|
|
||||||
const response: RPCState = {
|
|
||||||
event: payload.event,
|
|
||||||
uuid: payload.uuid,
|
|
||||||
calledFrom: 'webview',
|
|
||||||
calledTo: 'client',
|
|
||||||
error: null,
|
|
||||||
data: [responseData],
|
|
||||||
player: payload.player,
|
|
||||||
type: 'response',
|
|
||||||
}
|
|
||||||
|
|
||||||
await this._createHttpClientRequest(response).then()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,23 +57,7 @@ export class RPCInstanceWebview extends Wrapper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (payload.calledFrom === 'server' && payload.type === 'event') {
|
if (payload.calledFrom === 'server' && payload.type === 'event') {
|
||||||
this.verifyEvent(this._emitterServer, payload)
|
const response = await this.dispatch(this._emitterServer, payload)
|
||||||
|
|
||||||
const responseData = await this._emitterServer.emit(
|
|
||||||
payload.event,
|
|
||||||
...(payload.data && payload.data.length > 0 ? payload.data : []),
|
|
||||||
)
|
|
||||||
|
|
||||||
const response: RPCState = {
|
|
||||||
event: payload.event,
|
|
||||||
uuid: payload.uuid,
|
|
||||||
calledFrom: 'webview',
|
|
||||||
calledTo: 'server',
|
|
||||||
error: null,
|
|
||||||
data: [responseData],
|
|
||||||
player: payload.player,
|
|
||||||
type: 'response',
|
|
||||||
}
|
|
||||||
|
|
||||||
await this._createHttpClientRequest(response)
|
await this._createHttpClientRequest(response)
|
||||||
}
|
}
|
||||||
@@ -254,7 +222,7 @@ export class RPCInstanceWebview extends Wrapper {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.verifyEvent(this._emitterLocal, payload)
|
this.assertListener(this._emitterLocal, payload.event)
|
||||||
|
|
||||||
return await this._emitterLocal.emit<Awaited<Response>>(
|
return await this._emitterLocal.emit<Awaited<Response>>(
|
||||||
payload.event,
|
payload.event,
|
||||||
@@ -266,9 +234,9 @@ export class RPCInstanceWebview extends Wrapper {
|
|||||||
|
|
||||||
/** Sends an event to the client and waits for its response (with timeout) */
|
/** Sends an event to the client and waits for its response (with timeout) */
|
||||||
private _request<R>(payload: RPCState): Promise<R> {
|
private _request<R>(payload: RPCState): Promise<R> {
|
||||||
const response = this._pending.wait<R>(payload.uuid)
|
const response = this._pending.wait<R>(payload)
|
||||||
this._createHttpClientRequest(payload).then(
|
this._createHttpClientRequest<RPCState>(payload).then(
|
||||||
data => this._pending.resolve(payload.uuid, data),
|
res => this.settle(res),
|
||||||
(error: Error) => this._pending.reject(payload.uuid, error),
|
(error: Error) => this._pending.reject(payload.uuid, error),
|
||||||
)
|
)
|
||||||
return response
|
return response
|
||||||
|
|||||||
+94
-28
@@ -1,12 +1,15 @@
|
|||||||
import { Emitter } from '../utils/emitter'
|
import { Emitter } from '../utils/emitter'
|
||||||
import { parse } from '../utils/funcs'
|
import {
|
||||||
|
handlerErrorMessage,
|
||||||
|
notRegisteredMessage,
|
||||||
|
RPCError,
|
||||||
|
} from '../utils/errors'
|
||||||
import { Pending } from '../utils/pending'
|
import { Pending } from '../utils/pending'
|
||||||
import {
|
import {
|
||||||
type RPCConfig,
|
type RPCConfig,
|
||||||
type RPCEnvironment,
|
type RPCEnvironment,
|
||||||
RPCErrors,
|
RPCErrors,
|
||||||
type RPCState,
|
type RPCState,
|
||||||
type RPCStateRaw,
|
|
||||||
} from '../utils/types'
|
} from '../utils/types'
|
||||||
|
|
||||||
export class Wrapper {
|
export class Wrapper {
|
||||||
@@ -24,41 +27,104 @@ export class Wrapper {
|
|||||||
this.console = console
|
this.console = console
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Settles the call waiting for this response; ignores late or unexpected ones */
|
/**
|
||||||
protected resolvePending(payload: RPCState): void {
|
* Runs the listener for `request` and builds the response to send back.
|
||||||
const found = this._pending.resolve(payload.uuid, payload.data?.[0])
|
* Never throws: a missing listener or a thrown error ends up in `error`.
|
||||||
if (!found && this.debug) {
|
*
|
||||||
this.console.log(
|
* @param prefix - arguments passed before the request data (server: player)
|
||||||
`[RPC]:ignored response ${payload.event} ${payload.uuid} (no pending call, possibly timed out)`,
|
*/
|
||||||
|
protected async dispatch(
|
||||||
|
emitter: Emitter,
|
||||||
|
request: RPCState,
|
||||||
|
...prefix: unknown[]
|
||||||
|
): Promise<RPCState> {
|
||||||
|
if (!emitter.has(request.event)) {
|
||||||
|
return this.errorResponse(
|
||||||
|
request,
|
||||||
|
new RPCError(
|
||||||
|
RPCErrors.EVENT_NOT_REGISTERED,
|
||||||
|
notRegisteredMessage(request.event, this.env, request.calledFrom),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await emitter.emit(
|
||||||
|
request.event,
|
||||||
|
...prefix,
|
||||||
|
...(request.data ?? []),
|
||||||
|
)
|
||||||
|
return this.response(request, [data], null)
|
||||||
|
} catch (e) {
|
||||||
|
// keep the stack visible where the listener lives
|
||||||
|
this.console.error(e)
|
||||||
|
return this.errorResponse(
|
||||||
|
request,
|
||||||
|
new RPCError(
|
||||||
|
RPCErrors.HANDLER_ERROR,
|
||||||
|
handlerErrorMessage(request.event, this.env, e),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected verifyEvent(state: Emitter, data: RPCStateRaw | RPCState) {
|
/** Builds the response to `request` carrying `error` */
|
||||||
const rpcData = typeof data === 'string' ? parse(data) : data
|
protected errorResponse(request: RPCState, error: unknown): RPCState {
|
||||||
|
const rpcError =
|
||||||
|
error instanceof RPCError
|
||||||
|
? error
|
||||||
|
: new RPCError(
|
||||||
|
RPCErrors.HANDLER_ERROR,
|
||||||
|
handlerErrorMessage(request.event, this.env, error),
|
||||||
|
)
|
||||||
|
return this.response(request, null, {
|
||||||
|
code: rpcError.code,
|
||||||
|
message: rpcError.message,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if (!state.has(rpcData.event)) {
|
/** Settles the call waiting for `response`; ignores late or unexpected ones */
|
||||||
rpcData.error = RPCErrors.EVENT_NOT_REGISTERED
|
protected settle(response: RPCState): void {
|
||||||
this.triggerError(rpcData)
|
const found = response.error
|
||||||
|
? this._pending.reject(response.uuid, RPCError.fromResponse(response))
|
||||||
|
: this._pending.resolve(response.uuid, response.data?.[0])
|
||||||
|
|
||||||
|
if (!found) this.logIgnored(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Throws if no local listener is registered for `event` */
|
||||||
|
protected assertListener(emitter: Emitter, event: string): void {
|
||||||
|
if (!emitter.has(event)) {
|
||||||
|
throw new RPCError(
|
||||||
|
RPCErrors.EVENT_NOT_REGISTERED,
|
||||||
|
notRegisteredMessage(event, this.env, this.env),
|
||||||
|
{ event, uuid: '', from: this.env, to: this.env },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected triggerError(rpcData: RPCState, error?: string): Error {
|
protected logIgnored(response: RPCState): void {
|
||||||
const errorMessage = [
|
if (this.debug) {
|
||||||
`${rpcData.error}`,
|
this.console.log(
|
||||||
`Event: ${rpcData.event}`,
|
`[RPC]:ignored response ${response.event} ${response.uuid} (no pending call, possibly timed out)`,
|
||||||
`Uuid: ${rpcData.uuid}`,
|
)
|
||||||
`From: ${rpcData.calledFrom}`,
|
}
|
||||||
`To: ${rpcData.calledTo}`,
|
|
||||||
`Player: ${rpcData.player}`,
|
|
||||||
`Type: ${rpcData.type}`,
|
|
||||||
`Data: ${rpcData.data}`,
|
|
||||||
]
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
errorMessage.push(`Info: ${error}`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(errorMessage.join('\n | '))
|
private response(
|
||||||
|
request: RPCState,
|
||||||
|
data: RPCState['data'],
|
||||||
|
error: RPCState['error'],
|
||||||
|
): RPCState {
|
||||||
|
return {
|
||||||
|
event: request.event,
|
||||||
|
uuid: request.uuid,
|
||||||
|
calledFrom: this.env,
|
||||||
|
calledTo: request.calledFrom,
|
||||||
|
error,
|
||||||
|
data,
|
||||||
|
player: request.player,
|
||||||
|
type: 'response',
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-1
@@ -2,6 +2,7 @@ import { RPCInstanceClient } from './core/client'
|
|||||||
import { RPCInstanceServer } from './core/server'
|
import { RPCInstanceServer } from './core/server'
|
||||||
import { RPCInstanceWebview } from './core/webview'
|
import { RPCInstanceWebview } from './core/webview'
|
||||||
import { Wrapper } from './core/wrapper'
|
import { Wrapper } from './core/wrapper'
|
||||||
|
import { RPCError } from './utils/errors'
|
||||||
import {
|
import {
|
||||||
type RPCConfig,
|
type RPCConfig,
|
||||||
type RPCEnvironment,
|
type RPCEnvironment,
|
||||||
@@ -54,7 +55,10 @@ class RPCFactory<T extends RPCEnvironment> extends Wrapper {
|
|||||||
this.operator = new RPCInstanceWebview(opts as RPCConfig<'webview'>)
|
this.operator = new RPCInstanceWebview(opts as RPCConfig<'webview'>)
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
throw new Error(RPCErrors.UNKNOWN_ENVIRONMENT)
|
throw new RPCError(
|
||||||
|
RPCErrors.UNKNOWN_ENVIRONMENT,
|
||||||
|
RPCErrors.UNKNOWN_ENVIRONMENT,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,6 +69,7 @@ class RPCFactory<T extends RPCEnvironment> extends Wrapper {
|
|||||||
|
|
||||||
export { RPCFactory }
|
export { RPCFactory }
|
||||||
export * from './utils/types'
|
export * from './utils/types'
|
||||||
|
export { RPCError, type RPCErrorDetails } from './utils/errors'
|
||||||
export * from './utils/native'
|
export * from './utils/native'
|
||||||
export type * from './core/server'
|
export type * from './core/server'
|
||||||
export type * from './core/client'
|
export type * from './core/client'
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import { type RPCEnvironment, RPCErrors, type RPCState } from './types'
|
||||||
|
|
||||||
|
/** Where a failed call was going, when the error comes from a call */
|
||||||
|
export type RPCErrorDetails = {
|
||||||
|
event: string
|
||||||
|
uuid: string
|
||||||
|
/** Environment that made the call */
|
||||||
|
from: RPCEnvironment
|
||||||
|
/** Environment that was called */
|
||||||
|
to: RPCEnvironment
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error thrown or rejected by this library. Check `code` against `RPCErrors`.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* try {
|
||||||
|
* await rpc.emitServer('buyItem', 'water')
|
||||||
|
* } catch (e) {
|
||||||
|
* if (e instanceof RPCError && e.code === RPCErrors.TIMEOUT) {
|
||||||
|
* // server did not answer in time
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
export class RPCError extends Error {
|
||||||
|
public readonly code: RPCErrors
|
||||||
|
public readonly details: RPCErrorDetails | undefined
|
||||||
|
|
||||||
|
constructor(code: RPCErrors, message: string, details?: RPCErrorDetails) {
|
||||||
|
super(message)
|
||||||
|
this.name = 'RPCError'
|
||||||
|
this.code = code
|
||||||
|
this.details = details
|
||||||
|
}
|
||||||
|
|
||||||
|
/** **Internal** Rebuilds the error a receiver sent back in a response */
|
||||||
|
static fromResponse(response: RPCState): RPCError {
|
||||||
|
return new RPCError(
|
||||||
|
response.error?.code ?? RPCErrors.HANDLER_ERROR,
|
||||||
|
response.error?.message ?? 'Unknown error',
|
||||||
|
{
|
||||||
|
event: response.event,
|
||||||
|
uuid: response.uuid,
|
||||||
|
from: response.calledTo,
|
||||||
|
to: response.calledFrom,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** **Internal** Name of the method that listens on `receiver` for calls from `sender` */
|
||||||
|
export function listenerName(
|
||||||
|
receiver: RPCEnvironment,
|
||||||
|
sender: RPCEnvironment,
|
||||||
|
): string {
|
||||||
|
if (receiver === sender) return 'onSelf'
|
||||||
|
return `on${sender[0]?.toUpperCase()}${sender.slice(1)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/** **Internal** */
|
||||||
|
export function notRegisteredMessage(
|
||||||
|
event: string,
|
||||||
|
receiver: RPCEnvironment,
|
||||||
|
sender: RPCEnvironment,
|
||||||
|
): string {
|
||||||
|
return `No listener for "${event}" on ${receiver}. Register it with rpc.${listenerName(receiver, sender)}("${event}", ...) in ${receiver} code.`
|
||||||
|
}
|
||||||
|
|
||||||
|
/** **Internal** */
|
||||||
|
export function handlerErrorMessage(
|
||||||
|
event: string,
|
||||||
|
receiver: RPCEnvironment,
|
||||||
|
cause: unknown,
|
||||||
|
): string {
|
||||||
|
const reason =
|
||||||
|
cause instanceof Error || (cause as Error | undefined)?.message
|
||||||
|
? (cause as Error).message
|
||||||
|
: String(cause)
|
||||||
|
return `Listener for "${event}" on ${receiver} threw: ${reason}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/** **Internal** */
|
||||||
|
export function timeoutMessage(
|
||||||
|
event: string,
|
||||||
|
receiver: RPCEnvironment,
|
||||||
|
sender: RPCEnvironment,
|
||||||
|
timeout: number,
|
||||||
|
): string {
|
||||||
|
return `No response for "${event}" from ${receiver} within ${timeout} ms. Check that ${receiver} registered rpc.${listenerName(receiver, sender)}("${event}", ...) and that it returns, or raise RPCConfig.timeout (0 disables it).`
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { RPCErrors } from './types'
|
import { RPCError, timeoutMessage } from './errors'
|
||||||
|
import { RPCErrors, type RPCState } from './types'
|
||||||
|
|
||||||
type Call = {
|
type Call = {
|
||||||
resolve: (data: unknown) => void
|
resolve: (data: unknown) => void
|
||||||
@@ -13,17 +14,18 @@ export class Pending {
|
|||||||
/** @param timeout - ms before a call rejects, `0` or less disables it */
|
/** @param timeout - ms before a call rejects, `0` or less disables it */
|
||||||
constructor(private readonly _timeout: number) {}
|
constructor(private readonly _timeout: number) {}
|
||||||
|
|
||||||
public wait<R>(uuid: string): Promise<R> {
|
/** Waits for the response to `request`, rejects with `RPCErrors.TIMEOUT` */
|
||||||
|
public wait<R>(request: RPCState): Promise<R> {
|
||||||
return new Promise<R>((resolve, reject) => {
|
return new Promise<R>((resolve, reject) => {
|
||||||
const timer =
|
const timer =
|
||||||
this._timeout > 0
|
this._timeout > 0
|
||||||
? setTimeout(
|
? setTimeout(
|
||||||
() => this.reject(uuid, new Error(RPCErrors.TIMEOUT)),
|
() => this.reject(request.uuid, this._timeoutError(request)),
|
||||||
this._timeout,
|
this._timeout,
|
||||||
)
|
)
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
this._calls.set(uuid, {
|
this._calls.set(request.uuid, {
|
||||||
resolve: resolve as (data: unknown) => void,
|
resolve: resolve as (data: unknown) => void,
|
||||||
reject,
|
reject,
|
||||||
timer,
|
timer,
|
||||||
@@ -53,4 +55,22 @@ export class Pending {
|
|||||||
}
|
}
|
||||||
return call
|
return call
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _timeoutError(request: RPCState): RPCError {
|
||||||
|
return new RPCError(
|
||||||
|
RPCErrors.TIMEOUT,
|
||||||
|
timeoutMessage(
|
||||||
|
request.event,
|
||||||
|
request.calledTo,
|
||||||
|
request.calledFrom,
|
||||||
|
this._timeout,
|
||||||
|
),
|
||||||
|
{
|
||||||
|
event: request.event,
|
||||||
|
uuid: request.uuid,
|
||||||
|
from: request.calledFrom,
|
||||||
|
to: request.calledTo,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,12 +46,18 @@ export type RPCState = {
|
|||||||
uuid: string
|
uuid: string
|
||||||
calledFrom: RPCEnvironment
|
calledFrom: RPCEnvironment
|
||||||
calledTo: RPCEnvironment
|
calledTo: RPCEnvironment
|
||||||
error: string | null
|
error: RPCErrorPayload | null
|
||||||
data: unknown[] | null
|
data: unknown[] | null
|
||||||
player: number | null
|
player: number | null
|
||||||
type: RPCEventType
|
type: RPCEventType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** **Internal** Error sent back in a response, rebuilt as `RPCError` by the caller */
|
||||||
|
export type RPCErrorPayload = {
|
||||||
|
code: RPCErrors
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* **Internal**
|
* **Internal**
|
||||||
*
|
*
|
||||||
@@ -93,6 +99,7 @@ export enum RPCErrors {
|
|||||||
UNKNOWN_NATIVE = 'Unknown native event (if you are sure this exists - use native handler)',
|
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")',
|
UNKNOWN_ENVIRONMENT = 'Unknown environment (must be either "server", "client" or "webview")',
|
||||||
TIMEOUT = 'Timed out waiting for response',
|
TIMEOUT = 'Timed out waiting for response',
|
||||||
|
HANDLER_ERROR = 'Listener threw an error',
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user