fix(rpc): better error handling

This commit is contained in:
2026-09-28 02:05:40 +01:00
parent 79faa3b23c
commit 571d113386
8 changed files with 282 additions and 148 deletions
+94 -28
View File
@@ -1,12 +1,15 @@
import { Emitter } from '../utils/emitter'
import { parse } from '../utils/funcs'
import {
handlerErrorMessage,
notRegisteredMessage,
RPCError,
} from '../utils/errors'
import { Pending } from '../utils/pending'
import {
type RPCConfig,
type RPCEnvironment,
RPCErrors,
type RPCState,
type RPCStateRaw,
} from '../utils/types'
export class Wrapper {
@@ -24,41 +27,104 @@ export class Wrapper {
this.console = console
}
/** Settles the call waiting for this response; ignores late or unexpected ones */
protected resolvePending(payload: RPCState): void {
const found = this._pending.resolve(payload.uuid, payload.data?.[0])
if (!found && this.debug) {
this.console.log(
`[RPC]:ignored response ${payload.event} ${payload.uuid} (no pending call, possibly timed out)`,
/**
* 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`.
*
* @param prefix - arguments passed before the request data (server: player)
*/
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) {
const rpcData = typeof data === 'string' ? parse(data) : data
/** Builds the response to `request` carrying `error` */
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)) {
rpcData.error = RPCErrors.EVENT_NOT_REGISTERED
this.triggerError(rpcData)
/** Settles the call waiting for `response`; ignores late or unexpected ones */
protected settle(response: RPCState): void {
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 {
const errorMessage = [
`${rpcData.error}`,
`Event: ${rpcData.event}`,
`Uuid: ${rpcData.uuid}`,
`From: ${rpcData.calledFrom}`,
`To: ${rpcData.calledTo}`,
`Player: ${rpcData.player}`,
`Type: ${rpcData.type}`,
`Data: ${rpcData.data}`,
]
if (error) {
errorMessage.push(`Info: ${error}`)
protected logIgnored(response: RPCState): void {
if (this.debug) {
this.console.log(
`[RPC]:ignored response ${response.event} ${response.uuid} (no pending call, possibly timed out)`,
)
}
}
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',
}
}
}