mirror of
https://github.com/rilaxik/fivem-rpc.git
synced 2026-09-28 01:29:13 +00:00
init
This commit is contained in:
@@ -0,0 +1,402 @@
|
||||
/// <reference types="@citizenfx/client" />
|
||||
|
||||
import type * as s from '@entityseven/fivem-rpc-shared-types'
|
||||
import { Emitter } from '../utils/emitter'
|
||||
import { generateUUID, parse, stringify, stringifyWeb } from '../utils/funcs'
|
||||
import {
|
||||
NATIVE_CLIENT_EVENTS,
|
||||
NATIVE_CLIENT_NETWORK_EVENTS,
|
||||
} from '../utils/native'
|
||||
import {
|
||||
type RPCConfig,
|
||||
RPCErrors,
|
||||
RPCEvents,
|
||||
type RPCNativeClientEvents,
|
||||
type RPCNativeClientNetworksEvents,
|
||||
type RPCState,
|
||||
type RPCStateRaw,
|
||||
type RPCStateWeb,
|
||||
} from '../utils/types'
|
||||
import { Wrapper } from './wrapper'
|
||||
|
||||
export class RPCInstanceClient extends Wrapper {
|
||||
private readonly _emitterServer: Emitter
|
||||
private readonly _pendingServer: Emitter
|
||||
private readonly _emitterWeb: Emitter
|
||||
private readonly _pendingWeb: Emitter
|
||||
private readonly _pendingWebToServer: Emitter
|
||||
|
||||
constructor(props: RPCConfig<'client'>) {
|
||||
super(props)
|
||||
|
||||
this._emitterServer = new Emitter()
|
||||
this._pendingServer = new Emitter()
|
||||
this._emitterWeb = new Emitter()
|
||||
this._pendingWeb = new Emitter()
|
||||
this._pendingWebToServer = new Emitter()
|
||||
|
||||
this.console.log('[RPC] Initialized Client')
|
||||
|
||||
onNet(RPCEvents.LISTENER_SERVER, this._handleServer.bind(this))
|
||||
RegisterNuiCallbackType(RPCEvents.LISTENER_WEB)
|
||||
on(
|
||||
`__cfx_nui:${RPCEvents.LISTENER_WEB}`,
|
||||
async (data: RPCState, callback: (res: unknown) => void) => {
|
||||
const res = await this._handleWeb(data)
|
||||
callback(res)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// ===== HANDLERS =====
|
||||
|
||||
private async _handleServer(payloadRaw: RPCStateRaw) {
|
||||
try {
|
||||
parse(payloadRaw)
|
||||
} catch (e) {
|
||||
throw new Error(RPCErrors.INVALID_DATA)
|
||||
}
|
||||
const payload = parse(payloadRaw)
|
||||
|
||||
if (this.debug) {
|
||||
this.console.log(
|
||||
`[RPC]:client:accepted ${payload.type} ${payload.event} from ${payload.calledFrom}`,
|
||||
)
|
||||
}
|
||||
|
||||
if (payload.type === 'event') {
|
||||
if (payload.calledTo === 'client') {
|
||||
this.verifyEvent(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))
|
||||
}
|
||||
if (payload.calledTo === 'webview') {
|
||||
this._sendWebMessage({
|
||||
origin: RPCEvents.LISTENER_SERVER,
|
||||
data: payload,
|
||||
})
|
||||
}
|
||||
}
|
||||
if (payload.type === 'response') {
|
||||
if (payload.calledTo === 'client') {
|
||||
await this._pendingServer.emit(
|
||||
payload.uuid,
|
||||
...(payload.data && payload.data.length > 0 ? payload.data : []),
|
||||
)
|
||||
}
|
||||
if (payload.calledTo === 'webview') {
|
||||
await this._pendingWebToServer.emit(
|
||||
payload.uuid,
|
||||
...(payload.data && payload.data.length > 0 ? payload.data : []),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async _handleWeb(payload: RPCState): Promise<unknown> {
|
||||
if (this.debug) {
|
||||
this.console.log(
|
||||
`[RPC]:client:accepted ${payload.type} ${payload.event} from ${payload.calledFrom}`,
|
||||
)
|
||||
}
|
||||
|
||||
if (payload.type === 'event') {
|
||||
if (payload.calledTo === 'client') {
|
||||
return await this._emitterWeb.emit(
|
||||
payload.event,
|
||||
...(payload.data && payload.data.length > 0 ? payload.data : []),
|
||||
)
|
||||
}
|
||||
if (payload.calledTo === 'server') {
|
||||
payload.player = GetPlayerServerId(PlayerId())
|
||||
emitNet(RPCEvents.LISTENER_WEB, stringify(payload))
|
||||
|
||||
return new Promise(res => {
|
||||
this._pendingWebToServer.once(payload.uuid, res)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (payload.type === 'response') {
|
||||
if (payload.calledTo === 'client') {
|
||||
await this._pendingWeb.emit(
|
||||
payload.uuid,
|
||||
...(payload.data && payload.data.length > 0 ? payload.data : []),
|
||||
)
|
||||
|
||||
return { status: 'ok' }
|
||||
}
|
||||
if (payload.calledTo === 'server') {
|
||||
payload.player = GetPlayerServerId(PlayerId())
|
||||
emitNet(RPCEvents.LISTENER_WEB, stringify(payload))
|
||||
|
||||
return { status: 'ok' }
|
||||
}
|
||||
}
|
||||
return { status: 'unknown' }
|
||||
}
|
||||
|
||||
// ===== SERVER =====
|
||||
|
||||
public onServer<
|
||||
EventName extends keyof s.RPCEvents_ServerClient,
|
||||
CallbackArguments extends Parameters<s.RPCEvents_ServerClient[EventName]>,
|
||||
CallbackReturn extends ReturnType<s.RPCEvents_ServerClient[EventName]>,
|
||||
>(
|
||||
eventName: EventName,
|
||||
cb: (
|
||||
...args: CallbackArguments
|
||||
) => Awaited<CallbackReturn> | Promise<Awaited<CallbackReturn>>,
|
||||
): this {
|
||||
if (this.debug) {
|
||||
this.console.log(`[RPC]:onServer ${eventName}`)
|
||||
}
|
||||
|
||||
this._emitterServer.on(eventName, cb)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public offServer<EventName extends keyof s.RPCEvents_ServerClient>(
|
||||
eventName: EventName,
|
||||
): this {
|
||||
if (this.debug) {
|
||||
this.console.log(`[RPC]:offServer ${eventName}`)
|
||||
}
|
||||
|
||||
this._emitterServer.off(eventName)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public async emitServer<
|
||||
EventName extends keyof s.RPCEvents_ClientServer,
|
||||
Arguments extends Parameters<s.RPCEvents_ClientServer[EventName]>,
|
||||
Response extends ReturnType<s.RPCEvents_ClientServer[EventName]>,
|
||||
>(eventName: EventName, ...args: Arguments): Promise<Awaited<Response>> {
|
||||
const payload: RPCState = {
|
||||
event: eventName,
|
||||
uuid: generateUUID(),
|
||||
calledFrom: 'client',
|
||||
calledTo: 'server',
|
||||
error: null,
|
||||
data: args.length ? args : null,
|
||||
player: GetPlayerServerId(PlayerId()),
|
||||
type: 'event',
|
||||
}
|
||||
|
||||
emitNet(RPCEvents.LISTENER_CLIENT, stringify(payload))
|
||||
|
||||
return new Promise<Awaited<Response>>(res => {
|
||||
this._pendingServer.once(payload.uuid, res)
|
||||
})
|
||||
}
|
||||
|
||||
// ===== WEBVIEW =====
|
||||
|
||||
public onWebview<
|
||||
EventName extends keyof s.RPCEvents_WebviewClient,
|
||||
CallbackArguments extends Parameters<s.RPCEvents_WebviewClient[EventName]>,
|
||||
CallbackReturn extends ReturnType<s.RPCEvents_WebviewClient[EventName]>,
|
||||
>(
|
||||
eventName: EventName,
|
||||
cb: (
|
||||
...args: CallbackArguments
|
||||
) => Awaited<CallbackReturn> | Promise<Awaited<CallbackReturn>>,
|
||||
): this {
|
||||
if (this.debug) {
|
||||
this.console.log(`[RPC]:onWebview ${eventName}`)
|
||||
}
|
||||
|
||||
this._emitterWeb.on(eventName, cb)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public offWebview<EventName extends keyof s.RPCEvents_WebviewClient>(
|
||||
eventName: EventName,
|
||||
): this {
|
||||
if (this.debug) {
|
||||
this.console.log(`[RPC]:offWebview ${eventName}`)
|
||||
}
|
||||
|
||||
this._emitterWeb.off(eventName)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public async emitWebview<
|
||||
EventName extends keyof s.RPCEvents_ClientWebview,
|
||||
Arguments extends Parameters<s.RPCEvents_ClientWebview[EventName]>,
|
||||
Response extends ReturnType<s.RPCEvents_ClientWebview[EventName]>,
|
||||
>(eventName: EventName, ...args: Arguments): Promise<Awaited<Response>> {
|
||||
const payload: RPCState = {
|
||||
event: eventName,
|
||||
uuid: generateUUID(),
|
||||
calledFrom: 'client',
|
||||
calledTo: 'webview',
|
||||
error: null,
|
||||
data: args.length ? args : null,
|
||||
player: PlayerId(),
|
||||
type: 'event',
|
||||
}
|
||||
|
||||
this._sendWebMessage({
|
||||
origin: RPCEvents.LISTENER_CLIENT,
|
||||
data: payload,
|
||||
})
|
||||
|
||||
return new Promise<Awaited<Response>>(res => {
|
||||
this._pendingWeb.once(payload.uuid, res)
|
||||
})
|
||||
}
|
||||
|
||||
// ===== SELF =====
|
||||
|
||||
public onSelf<
|
||||
EventName extends keyof s.RPCEvents_Client,
|
||||
CallbackArguments extends Parameters<s.RPCEvents_Client[EventName]>,
|
||||
CallbackReturn extends ReturnType<s.RPCEvents_Client[EventName]>,
|
||||
>(
|
||||
eventName: EventName,
|
||||
cb: (
|
||||
...args: CallbackArguments
|
||||
) => Awaited<CallbackReturn> | Promise<Awaited<CallbackReturn>>,
|
||||
): this {
|
||||
if (this.debug) {
|
||||
this.console.log(`[RPC]:onSelf ${eventName}`)
|
||||
}
|
||||
|
||||
this._emitterLocal.on(eventName, cb)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public offSelf<EventName extends keyof s.RPCEvents_Client>(
|
||||
eventName: EventName,
|
||||
): this {
|
||||
if (this.debug) {
|
||||
this.console.log(`[RPC]:offSelf ${eventName}`)
|
||||
}
|
||||
|
||||
this._emitterLocal.off(eventName)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public async emitSelf<
|
||||
EventName extends keyof s.RPCEvents_Client,
|
||||
Arguments extends Parameters<s.RPCEvents_Client[EventName]>,
|
||||
Response extends ReturnType<s.RPCEvents_Client[EventName]>,
|
||||
>(eventName: EventName, ...args: Arguments): Promise<Awaited<Response>> {
|
||||
const payload: RPCState = {
|
||||
event: eventName,
|
||||
uuid: generateUUID(),
|
||||
calledFrom: 'client',
|
||||
calledTo: 'client',
|
||||
error: null,
|
||||
data: args.length ? args : null,
|
||||
player: null,
|
||||
type: 'event',
|
||||
}
|
||||
|
||||
if (this.debug) {
|
||||
this.console.log(
|
||||
`[RPC]:accepted ${payload.event} from ${payload.calledFrom}`,
|
||||
)
|
||||
}
|
||||
|
||||
this.verifyEvent(this._emitterLocal, payload)
|
||||
|
||||
return await this._emitterLocal.emit<Awaited<Response>>(
|
||||
payload.event,
|
||||
...(payload.data && payload.data.length > 0 ? payload.data : []),
|
||||
)
|
||||
}
|
||||
|
||||
// ===== OTHER =====
|
||||
|
||||
public onCommand<
|
||||
CommandName extends s.RPCCommands_Client,
|
||||
CallbackArguments extends unknown[],
|
||||
>(
|
||||
command: CommandName,
|
||||
cb: (player: number, args: CallbackArguments, commandRaw: string) => void,
|
||||
): this {
|
||||
if (this.debug) {
|
||||
this.console.log(`[RPC]:onCommand ${command}`)
|
||||
}
|
||||
|
||||
RegisterCommand(command, cb, false)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public onNativeEvent<
|
||||
EventName extends keyof RPCNativeClientEvents,
|
||||
CallbackArguments extends Parameters<RPCNativeClientEvents[EventName]>,
|
||||
>(eventName: EventName, cb: (...args: CallbackArguments) => void): this {
|
||||
if (!NATIVE_CLIENT_EVENTS.includes(eventName)) {
|
||||
throw new Error(RPCErrors.UNKNOWN_NATIVE)
|
||||
}
|
||||
|
||||
if (this.debug) {
|
||||
this.console.log(`[RPC]:onNativeEvent ${eventName}`)
|
||||
}
|
||||
|
||||
on(eventName, cb)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public onNativeNetworkEvent<
|
||||
EventName extends keyof RPCNativeClientNetworksEvents,
|
||||
CallbackArguments extends Parameters<
|
||||
RPCNativeClientNetworksEvents[EventName]
|
||||
>,
|
||||
>(eventName: EventName, cb: (...args: CallbackArguments) => void): this {
|
||||
if (!NATIVE_CLIENT_NETWORK_EVENTS.includes(eventName)) {
|
||||
throw new Error(RPCErrors.UNKNOWN_NATIVE)
|
||||
}
|
||||
|
||||
if (this.debug) {
|
||||
this.console.log(`[RPC]:onNativeNetworkEvent ${eventName}`)
|
||||
}
|
||||
|
||||
on(eventName, cb)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public setWebviewFocus(hasFocus: boolean, hasCursor: boolean): this {
|
||||
if (this.debug) {
|
||||
this.console.log(`[RPC]:setWebviewFocus ${hasFocus} ${hasCursor}`)
|
||||
}
|
||||
|
||||
SetNuiFocus(hasFocus, hasCursor)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
// ===== UTILS =====
|
||||
|
||||
private _sendWebMessage(payload: RPCStateWeb): void {
|
||||
SendNuiMessage(stringifyWeb(payload))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,379 @@
|
||||
/// <reference types="@citizenfx/server" />
|
||||
|
||||
import type * as s from '@entityseven/fivem-rpc-shared-types'
|
||||
import { Emitter } from '../utils/emitter'
|
||||
import { generateUUID, parse, stringify } from '../utils/funcs'
|
||||
import { NATIVE_SERVER_EVENTS } from '../utils/native'
|
||||
import {
|
||||
type RPCConfig,
|
||||
RPCErrors,
|
||||
RPCEvents,
|
||||
type RPCNativeServerEvents,
|
||||
type RPCState,
|
||||
type RPCStateRaw,
|
||||
} from '../utils/types'
|
||||
import { Wrapper } from './wrapper'
|
||||
|
||||
export class RPCInstanceServer extends Wrapper {
|
||||
private readonly _emitterClient: Emitter
|
||||
private readonly _pendingClient: Emitter
|
||||
private readonly _emitterWeb: Emitter
|
||||
private readonly _pendingWeb: Emitter
|
||||
|
||||
constructor(props: RPCConfig<'server'>) {
|
||||
super(props)
|
||||
|
||||
this._emitterClient = new Emitter()
|
||||
this._pendingClient = new Emitter()
|
||||
this._emitterWeb = new Emitter()
|
||||
this._pendingWeb = new Emitter()
|
||||
|
||||
this.console.log('[RPC] Initialized Server')
|
||||
|
||||
onNet(RPCEvents.LISTENER_CLIENT, this._handleClient.bind(this))
|
||||
onNet(RPCEvents.LISTENER_WEB, this._handleWeb.bind(this))
|
||||
}
|
||||
|
||||
// ===== HANDLERS =====
|
||||
|
||||
private async _handleClient(payloadRaw: RPCStateRaw) {
|
||||
try {
|
||||
parse(payloadRaw)
|
||||
} catch (e) {
|
||||
throw new Error(RPCErrors.INVALID_DATA)
|
||||
}
|
||||
const payload = parse(payloadRaw)
|
||||
|
||||
if (this.debug) {
|
||||
this.console.log(
|
||||
`[RPC]:server:accepted ${payload.type} ${payload.event} from ${payload.calledFrom}`,
|
||||
)
|
||||
}
|
||||
|
||||
if (payload.calledFrom === 'client') {
|
||||
if (payload.type === 'event') {
|
||||
this.verifyEvent(this._emitterClient, payload)
|
||||
if (payload.player === null || payload.player === -1) {
|
||||
payload.error = RPCErrors.NO_PLAYER
|
||||
this.triggerError(payload)
|
||||
return
|
||||
}
|
||||
|
||||
const responseData = await this._emitterClient.emit(
|
||||
payload.event,
|
||||
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))
|
||||
}
|
||||
if (payload.type === 'response') {
|
||||
await this._pendingClient.emit(
|
||||
payload.uuid,
|
||||
...(payload.data && payload.data.length > 0 ? payload.data : []),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async _handleWeb(payloadRaw: RPCStateRaw) {
|
||||
try {
|
||||
parse(payloadRaw)
|
||||
} catch (e) {
|
||||
throw new Error(RPCErrors.INVALID_DATA)
|
||||
}
|
||||
const payload = parse(payloadRaw)
|
||||
|
||||
if (this.debug) {
|
||||
this.console.log(
|
||||
`[RPC]:server:accepted ${payload.type} ${payload.event} from ${payload.calledFrom}`,
|
||||
)
|
||||
}
|
||||
|
||||
if (payload.calledFrom === 'webview') {
|
||||
if (payload.type === 'event') {
|
||||
this.verifyEvent(this._emitterWeb, payload)
|
||||
if (payload.player === null || payload.player === -1) {
|
||||
payload.error = RPCErrors.NO_PLAYER
|
||||
this.triggerError(payload)
|
||||
return
|
||||
}
|
||||
|
||||
const responseData = await this._emitterWeb.emit(
|
||||
payload.event,
|
||||
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))
|
||||
}
|
||||
if (payload.type === 'response') {
|
||||
await this._pendingWeb.emit(
|
||||
payload.uuid,
|
||||
...(payload.data && payload.data.length > 0 ? payload.data : []),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===== CLIENT =====
|
||||
|
||||
public onClient<
|
||||
EventName extends keyof s.RPCEvents_ClientServer,
|
||||
CallbackArguments extends Parameters<s.RPCEvents_ClientServer[EventName]>,
|
||||
CallbackReturn extends ReturnType<s.RPCEvents_ClientServer[EventName]>,
|
||||
>(
|
||||
eventName: EventName,
|
||||
cb: (
|
||||
player: number,
|
||||
...args: CallbackArguments
|
||||
) => Awaited<CallbackReturn> | Promise<Awaited<CallbackReturn>>,
|
||||
): this {
|
||||
if (this.debug) {
|
||||
this.console.log(`[RPC]:onClient ${eventName}`)
|
||||
}
|
||||
|
||||
this._emitterClient.on(eventName, cb)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public offClient<EventName extends keyof s.RPCEvents_ClientServer>(
|
||||
eventName: EventName,
|
||||
): this {
|
||||
if (this.debug) {
|
||||
this.console.log(`[RPC]:offClient ${eventName}`)
|
||||
}
|
||||
|
||||
this._emitterClient.off(eventName)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public async emitClient<
|
||||
EventName extends keyof s.RPCEvents_ServerClient,
|
||||
Arguments extends Parameters<s.RPCEvents_ServerClient[EventName]>,
|
||||
Response extends ReturnType<s.RPCEvents_ServerClient[EventName]>,
|
||||
>(
|
||||
player: number,
|
||||
eventName: EventName,
|
||||
...args: Arguments
|
||||
): Promise<Awaited<Response>> {
|
||||
const payload: RPCState = {
|
||||
event: eventName,
|
||||
uuid: generateUUID(),
|
||||
calledFrom: 'server',
|
||||
calledTo: 'client',
|
||||
error: null,
|
||||
data: args.length ? args : null,
|
||||
player: player,
|
||||
type: 'event',
|
||||
}
|
||||
|
||||
emitNet(RPCEvents.LISTENER_SERVER, player, stringify(payload))
|
||||
|
||||
return new Promise<Awaited<Response>>(res => {
|
||||
this._pendingClient.once(payload.uuid, res)
|
||||
})
|
||||
}
|
||||
|
||||
public async emitClientEveryone<
|
||||
EventName extends keyof s.RPCEvents_ServerClient,
|
||||
Arguments extends Parameters<s.RPCEvents_ServerClient[EventName]>,
|
||||
>(eventName: EventName, ...args: Arguments): Promise<void> {
|
||||
const payload: RPCState = {
|
||||
event: eventName,
|
||||
uuid: generateUUID(),
|
||||
calledFrom: 'server',
|
||||
calledTo: 'client',
|
||||
error: null,
|
||||
data: args.length ? args : null,
|
||||
player: -1,
|
||||
type: 'event',
|
||||
}
|
||||
|
||||
emitNet(RPCEvents.LISTENER_SERVER, -1, stringify(payload))
|
||||
}
|
||||
|
||||
// ===== WEBVIEW =====
|
||||
|
||||
public onWebview<
|
||||
EventName extends keyof s.RPCEvents_WebviewServer,
|
||||
CallbackArguments extends Parameters<s.RPCEvents_WebviewServer[EventName]>,
|
||||
CallbackReturn extends ReturnType<s.RPCEvents_WebviewServer[EventName]>,
|
||||
>(
|
||||
eventName: EventName,
|
||||
cb: (
|
||||
player: number,
|
||||
...args: CallbackArguments
|
||||
) => Awaited<CallbackReturn> | Promise<Awaited<CallbackReturn>>,
|
||||
): this {
|
||||
if (this.debug) {
|
||||
this.console.log(`[RPC]:onWebview ${eventName}`)
|
||||
}
|
||||
|
||||
this._emitterWeb.on(eventName, cb)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public offWebview<EventName extends keyof s.RPCEvents_WebviewServer>(
|
||||
eventName: EventName,
|
||||
): this {
|
||||
if (this.debug) {
|
||||
this.console.log(`[RPC]:offWebview ${eventName}`)
|
||||
}
|
||||
|
||||
this._emitterWeb.off(eventName)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public async emitWebview<
|
||||
EventName extends keyof s.RPCEvents_ServerWebview,
|
||||
Arguments extends Parameters<s.RPCEvents_ServerWebview[EventName]>,
|
||||
Response extends ReturnType<s.RPCEvents_ServerWebview[EventName]>,
|
||||
>(
|
||||
player: number,
|
||||
eventName: EventName,
|
||||
...args: Arguments
|
||||
): Promise<Awaited<Response>> {
|
||||
const payload: RPCState = {
|
||||
event: eventName,
|
||||
uuid: generateUUID(),
|
||||
calledFrom: 'server',
|
||||
calledTo: 'webview',
|
||||
error: null,
|
||||
data: args.length ? args : null,
|
||||
player: player,
|
||||
type: 'event',
|
||||
}
|
||||
|
||||
emitNet(RPCEvents.LISTENER_SERVER, player, stringify(payload))
|
||||
|
||||
return new Promise<Awaited<Response>>(res => {
|
||||
this._pendingWeb.once(payload.uuid, res)
|
||||
})
|
||||
}
|
||||
|
||||
// ===== SELF =====
|
||||
|
||||
public onSelf<
|
||||
EventName extends keyof s.RPCEvents_Server,
|
||||
CallbackArguments extends Parameters<s.RPCEvents_Server[EventName]>,
|
||||
CallbackReturn extends ReturnType<s.RPCEvents_Server[EventName]>,
|
||||
>(
|
||||
eventName: EventName,
|
||||
cb: (
|
||||
...args: CallbackArguments
|
||||
) => Awaited<CallbackReturn> | Promise<Awaited<CallbackReturn>>,
|
||||
): this {
|
||||
if (this.debug) {
|
||||
this.console.log(`[RPC]:onSelf ${eventName}`)
|
||||
}
|
||||
|
||||
this._emitterLocal.on(eventName, cb)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public offSelf<EventName extends keyof s.RPCEvents_Server>(
|
||||
eventName: EventName,
|
||||
): this {
|
||||
if (this.debug) {
|
||||
this.console.log(`[RPC]:offSelf ${eventName}`)
|
||||
}
|
||||
|
||||
this._emitterLocal.off(eventName)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public async emitSelf<
|
||||
EventName extends keyof s.RPCEvents_Server,
|
||||
Arguments extends Parameters<s.RPCEvents_Server[EventName]>,
|
||||
Response extends ReturnType<s.RPCEvents_Server[EventName]>,
|
||||
>(eventName: EventName, ...args: Arguments): Promise<Awaited<Response>> {
|
||||
const payload: RPCState = {
|
||||
event: eventName,
|
||||
uuid: generateUUID(),
|
||||
calledFrom: 'server',
|
||||
calledTo: 'server',
|
||||
error: null,
|
||||
data: args.length ? args : null,
|
||||
player: null,
|
||||
type: 'event',
|
||||
}
|
||||
|
||||
if (this.debug) {
|
||||
this.console.log(
|
||||
`[RPC]:accepted ${payload.event} from ${payload.calledFrom}`,
|
||||
)
|
||||
}
|
||||
|
||||
this.verifyEvent(this._emitterLocal, payload)
|
||||
|
||||
return await this._emitterLocal.emit<Awaited<Response>>(
|
||||
payload.event,
|
||||
...(payload.data && payload.data.length > 0 ? payload.data : []),
|
||||
)
|
||||
}
|
||||
|
||||
// ===== OTHER =====
|
||||
|
||||
public onCommand<
|
||||
CommandName extends s.RPCCommands_Server,
|
||||
CallbackArguments extends unknown[],
|
||||
>(
|
||||
command: CommandName,
|
||||
cb: (player: number, args: CallbackArguments, commandRaw: string) => void,
|
||||
restricted = false,
|
||||
): this {
|
||||
if (this.debug) {
|
||||
this.console.log(`[RPC]:onCommand ${command}`)
|
||||
}
|
||||
|
||||
RegisterCommand(command, cb, restricted)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public onNativeEvent<
|
||||
EventName extends keyof RPCNativeServerEvents,
|
||||
CallbackArguments extends Parameters<RPCNativeServerEvents[EventName]>,
|
||||
>(eventName: EventName, cb: (...args: CallbackArguments) => void): this {
|
||||
if (!NATIVE_SERVER_EVENTS.includes(eventName)) {
|
||||
throw new Error(RPCErrors.UNKNOWN_NATIVE)
|
||||
}
|
||||
|
||||
if (this.debug) {
|
||||
this.console.log(`[RPC]:onNativeEvent ${eventName}`)
|
||||
}
|
||||
|
||||
on(eventName, cb)
|
||||
|
||||
return this
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,289 @@
|
||||
import type * as s from '@entityseven/fivem-rpc-shared-types'
|
||||
import { Emitter } from '../utils/emitter'
|
||||
import { generateUUID, stringify } from '../utils/funcs'
|
||||
import {
|
||||
RPCEvents,
|
||||
type RPCConfig,
|
||||
type RPCState,
|
||||
type RPCStateRaw,
|
||||
type RPCStateWeb,
|
||||
} from '../utils/types'
|
||||
import { Wrapper } from './wrapper'
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
GetParentResourceName?: () => string
|
||||
}
|
||||
}
|
||||
|
||||
export class RPCInstanceWebview extends Wrapper {
|
||||
private readonly _emitterClient: Emitter
|
||||
private readonly _emitterServer: Emitter
|
||||
|
||||
constructor(props: RPCConfig<'webview'>) {
|
||||
super(props)
|
||||
|
||||
this._emitterClient = new Emitter()
|
||||
this._emitterServer = new Emitter()
|
||||
|
||||
this.console.log('[RPC] Initialized Webview')
|
||||
|
||||
window.addEventListener('message', (e: MessageEvent<RPCStateWeb>) => {
|
||||
if (e.data.origin === RPCEvents.LISTENER_CLIENT) {
|
||||
this._handleClient(e.data.data)
|
||||
}
|
||||
if (e.data.origin === RPCEvents.LISTENER_SERVER) {
|
||||
this._handleServer(e.data.data)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ===== HANDLERS =====
|
||||
|
||||
private async _handleClient(payload: RPCState) {
|
||||
if (this.debug) {
|
||||
this.console.log(
|
||||
`[RPC]:webview:accepted ${payload.type} ${payload.event} from ${payload.calledFrom}`,
|
||||
)
|
||||
}
|
||||
|
||||
if (payload.calledFrom === 'client' && payload.type === 'event') {
|
||||
this.verifyEvent(this._emitterClient, payload)
|
||||
|
||||
const responseData = await this._emitterClient.emit(
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
private async _handleServer(payload: RPCState) {
|
||||
if (this.debug) {
|
||||
this.console.log(
|
||||
`[RPC]:webview:accepted ${payload.type} ${payload.event} from ${payload.calledFrom}`,
|
||||
)
|
||||
}
|
||||
|
||||
if (payload.calledFrom === 'server' && payload.type === 'event') {
|
||||
this.verifyEvent(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)
|
||||
}
|
||||
}
|
||||
|
||||
// ===== CLIENT =====
|
||||
|
||||
public onClient<
|
||||
EventName extends keyof s.RPCEvents_ClientWebview,
|
||||
CallbackArguments extends Parameters<s.RPCEvents_ClientWebview[EventName]>,
|
||||
CallbackReturn extends ReturnType<s.RPCEvents_ClientWebview[EventName]>,
|
||||
>(
|
||||
eventName: EventName,
|
||||
cb: (
|
||||
...args: CallbackArguments
|
||||
) => Awaited<CallbackReturn> | Promise<Awaited<CallbackReturn>>,
|
||||
): this {
|
||||
if (this.debug) {
|
||||
this.console.log(`[RPC]:onClient ${eventName}`)
|
||||
}
|
||||
|
||||
this._emitterClient.on(eventName, cb)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public offClient<EventName extends keyof s.RPCEvents_ClientWebview>(
|
||||
eventName: EventName,
|
||||
): this {
|
||||
if (this.debug) {
|
||||
this.console.log(`[RPC]:offClient ${eventName}`)
|
||||
}
|
||||
|
||||
this._emitterClient.off(eventName)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public async emitClient<
|
||||
EventName extends keyof s.RPCEvents_WebviewClient,
|
||||
Arguments extends Parameters<s.RPCEvents_WebviewClient[EventName]>,
|
||||
Response extends ReturnType<s.RPCEvents_WebviewClient[EventName]>,
|
||||
>(eventName: EventName, ...args: Arguments): Promise<Awaited<Response>> {
|
||||
const payload: RPCState = {
|
||||
event: eventName,
|
||||
uuid: generateUUID(),
|
||||
calledFrom: 'webview',
|
||||
calledTo: 'client',
|
||||
error: null,
|
||||
data: args.length ? args : null,
|
||||
player: null,
|
||||
type: 'event',
|
||||
}
|
||||
|
||||
return await this._createHttpClientRequest<Awaited<Response>>(payload)
|
||||
}
|
||||
|
||||
// ===== SERVER =====
|
||||
|
||||
public onServer<
|
||||
EventName extends keyof s.RPCEvents_ServerWebview,
|
||||
CallbackArguments extends Parameters<s.RPCEvents_ServerWebview[EventName]>,
|
||||
CallbackReturn extends ReturnType<s.RPCEvents_ServerWebview[EventName]>,
|
||||
>(
|
||||
eventName: EventName,
|
||||
cb: (
|
||||
...args: CallbackArguments
|
||||
) => Awaited<CallbackReturn> | Promise<Awaited<CallbackReturn>>,
|
||||
): this {
|
||||
if (this.debug) {
|
||||
this.console.log(`[RPC]:onServer ${eventName}`)
|
||||
}
|
||||
|
||||
this._emitterServer.on(eventName, cb)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public offServer<EventName extends keyof s.RPCEvents_ServerWebview>(
|
||||
eventName: EventName,
|
||||
): RPCInstanceWebview {
|
||||
if (this.debug) {
|
||||
this.console.log(`[RPC]:offServer ${eventName}`)
|
||||
}
|
||||
|
||||
this._emitterServer.off(eventName)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public async emitServer<
|
||||
EventName extends keyof s.RPCEvents_WebviewServer,
|
||||
Arguments extends Parameters<s.RPCEvents_WebviewServer[EventName]>,
|
||||
Response extends ReturnType<s.RPCEvents_WebviewServer[EventName]>,
|
||||
>(eventName: EventName, ...args: Arguments): Promise<Awaited<Response>> {
|
||||
const payload: RPCState = {
|
||||
event: eventName,
|
||||
uuid: generateUUID(),
|
||||
calledFrom: 'webview',
|
||||
calledTo: 'server',
|
||||
error: null,
|
||||
data: args.length ? args : null,
|
||||
player: null,
|
||||
type: 'event',
|
||||
}
|
||||
|
||||
return await this._createHttpClientRequest<Awaited<Response>>(payload)
|
||||
}
|
||||
|
||||
// ===== SELF =====
|
||||
|
||||
public onSelf<
|
||||
EventName extends keyof s.RPCEvents_Webview,
|
||||
CallbackArguments extends Parameters<s.RPCEvents_Webview[EventName]>,
|
||||
CallbackReturn extends ReturnType<s.RPCEvents_Webview[EventName]>,
|
||||
>(
|
||||
eventName: EventName,
|
||||
cb: (
|
||||
...args: CallbackArguments
|
||||
) => Awaited<CallbackReturn> | Promise<Awaited<CallbackReturn>>,
|
||||
): this {
|
||||
if (this.debug) {
|
||||
this.console.log(`[RPC]:onSelf ${eventName}`)
|
||||
}
|
||||
|
||||
this._emitterLocal.on(eventName, cb)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public offSelf<EventName extends keyof s.RPCEvents_Webview>(
|
||||
eventName: EventName,
|
||||
): this {
|
||||
if (this.debug) {
|
||||
this.console.log(`[RPC]:offSelf ${eventName}`)
|
||||
}
|
||||
|
||||
this._emitterLocal.off(eventName)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public async emitSelf<
|
||||
EventName extends keyof s.RPCEvents_Webview,
|
||||
Arguments extends Parameters<s.RPCEvents_Webview[EventName]>,
|
||||
Response extends ReturnType<s.RPCEvents_Webview[EventName]>,
|
||||
>(eventName: EventName, ...args: Arguments): Promise<Awaited<Response>> {
|
||||
const payload: RPCState = {
|
||||
event: eventName,
|
||||
uuid: generateUUID(),
|
||||
calledFrom: 'webview',
|
||||
calledTo: 'webview',
|
||||
error: null,
|
||||
data: args.length ? args : null,
|
||||
player: null,
|
||||
type: 'event',
|
||||
}
|
||||
|
||||
if (this.debug) {
|
||||
this.console.log(
|
||||
`[RPC]:accepted ${payload.event} from ${payload.calledFrom}`,
|
||||
)
|
||||
}
|
||||
|
||||
this.verifyEvent(this._emitterLocal, payload)
|
||||
|
||||
return await this._emitterLocal.emit<Awaited<Response>>(
|
||||
payload.event,
|
||||
...(payload.data && payload.data.length > 0 ? payload.data : []),
|
||||
)
|
||||
}
|
||||
|
||||
// ===== UTILS =====
|
||||
|
||||
private async _createHttpClientRequest<R>(
|
||||
data: RPCStateRaw | RPCState,
|
||||
): Promise<R> {
|
||||
const dataRaw = typeof data === 'string' ? data : stringify(data)
|
||||
const options = {
|
||||
method: 'post',
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=UTF-8',
|
||||
},
|
||||
body: dataRaw,
|
||||
}
|
||||
const resourceName = window?.GetParentResourceName?.() ?? 'nui-frame-app'
|
||||
return fetch(
|
||||
`https://${resourceName}/${RPCEvents.LISTENER_WEB}`,
|
||||
options,
|
||||
).then(res => res.json())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { Emitter } from '../utils/emitter'
|
||||
import { parse } from '../utils/funcs'
|
||||
import {
|
||||
type RPCConfig,
|
||||
type RPCEnvironment,
|
||||
RPCErrors,
|
||||
type RPCState,
|
||||
type RPCStateRaw,
|
||||
} from '../utils/types'
|
||||
|
||||
export class Wrapper {
|
||||
protected env: RPCEnvironment
|
||||
protected _emitterLocal: Emitter
|
||||
protected debug: boolean
|
||||
protected console: Console
|
||||
|
||||
constructor(cfg: RPCConfig<RPCEnvironment>) {
|
||||
this.env = cfg.env
|
||||
this._emitterLocal = new Emitter()
|
||||
this.debug = cfg.debug ?? false
|
||||
this.console = console
|
||||
}
|
||||
|
||||
protected verifyEvent(state: Emitter, data: RPCStateRaw | RPCState) {
|
||||
const rpcData = typeof data === 'string' ? parse(data) : data
|
||||
|
||||
if (!state.has(rpcData.event)) {
|
||||
rpcData.error = RPCErrors.EVENT_NOT_REGISTERED
|
||||
this.triggerError(rpcData)
|
||||
}
|
||||
}
|
||||
|
||||
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}`)
|
||||
}
|
||||
|
||||
throw new Error(errorMessage.join('\n | '))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user