diff --git a/rpc/.prettierrc.yaml b/rpc/.prettierrc.yaml new file mode 100644 index 0000000..aa7ff98 --- /dev/null +++ b/rpc/.prettierrc.yaml @@ -0,0 +1,6 @@ +tabWidth: 4 +printWidth: 80 +singleQuote: true +semi: false +arrowParens: avoid +endOfLine: auto diff --git a/rpc/package.json b/rpc/package.json new file mode 100644 index 0000000..18b9266 --- /dev/null +++ b/rpc/package.json @@ -0,0 +1,22 @@ +{ + "name": "rpc", + "version": "0.1.0", + "main": "dist/index.js", + "types": "dist/src/index.d.ts", + "scripts": { + "watch": "tsc -w", + "build": "tsup", + "start": "npx ./dist create" + }, + "files": [ + "dist/**/*" + ], + "description": "CLI to scaffold a template project for RageFW", + "keywords": [], + "author": "rilaxik", + "license": "ISC", + "devDependencies": { + "prettier": "^3.3.2", + "typescript": "^5.4.5" + } +} diff --git a/rpc/src/events.ts b/rpc/src/events.ts new file mode 100644 index 0000000..be9bc5f --- /dev/null +++ b/rpc/src/events.ts @@ -0,0 +1,2 @@ +export const EVENT_LISTENER = '__rpc:listener' +export const EVENT_RESPONSE = '__rpc:response' diff --git a/rpc/src/index.ts b/rpc/src/index.ts new file mode 100644 index 0000000..9bd2376 --- /dev/null +++ b/rpc/src/index.ts @@ -0,0 +1,61 @@ +import { Environment, utils } from './utils' +import { EVENT_LISTENER } from './events' + +import { client } from './modules/client' +import { server } from './modules/server' + +const environment = utils.getEnvironment() + +const state = environment === Environment.CEF ? window : global + +class rpc { + constructor() { + if (environment === Environment.UNKNOWN) return + + mp.events.add(EVENT_LISTENER, async (player: any, request: string) => { + switch (environment) { + case Environment.SERVER: + await server.listenEvent(player, request) + break + + case Environment.CLIENT: + request = player + + await client.listenEvent(request) + break + } + }) + } + + public register( + eventName: string, + cb: (...args: Callback) => Return, + ) { + if (environment === Environment.UNKNOWN) return + state[eventName] = cb + } + + public async callClient( + player: any, + eventName: string, + ...args: Args + ): Promise { + if (environment === Environment.UNKNOWN) return + if (environment === Environment.SERVER) { + return client.executeClient(player, eventName, args) + } + } + + public async callServer( + eventName: string, + ...args: Args + ): Promise { + if (environment === Environment.UNKNOWN) return + if (environment === Environment.CLIENT) { + return server.executeServer(eventName, args) + } + } +} + +const testRpc = new rpc() +export { testRpc } diff --git a/rpc/src/modules/client.ts b/rpc/src/modules/client.ts new file mode 100644 index 0000000..ae7489e --- /dev/null +++ b/rpc/src/modules/client.ts @@ -0,0 +1,94 @@ +import { EVENT_LISTENER } from '../events' +import { Wrapper } from './wrapper' +import { RPCState } from '../utils' + +class Client extends Wrapper { + private sendResponseToServer(data: RPCState) { + const eventName = this._utils.generateResponseEventName(data.uuid) + const preparedData = this._utils.prepareForTransfer(data) + mp.events.callRemote(eventName, preparedData) + } + + public async listenEvent(data: string) { + const rpcData = this._verifyEvent(data) + + if (rpcData.knownError) { + this._triggerError(rpcData) + return + } + + try { + const fnResponse = await this._state[rpcData.eventName]( + ...rpcData.data, + ) + const response = { + ...rpcData, + data: fnResponse, + } + + this.sendResponseToServer(response) + } catch (e) { + this._triggerError(rpcData, e) + } + } + + private handleClientServerReturn( + uuid: string, + resolve: (value: unknown) => void, + reject: (reason?: any) => void, + ) { + const responseEvent = this._utils.generateResponseEventName(uuid) + const timeoutDuration = 1000 * 10 + + const timeoutID = setTimeout(() => { + reject(new Error('Timeout ended')) + mp.events.remove(responseEvent) + }, timeoutDuration) + + const handler = (_: any, response: string) => { + const { knownError, data } = this._utils.prepareForExecute(response) + + if (knownError) + try { + clearTimeout(timeoutID) + reject(knownError) + return + } catch (e) {} + + resolve(data) + mp.events.remove(responseEvent) + + try { + clearTimeout(timeoutID) + } catch (e) {} + } + + mp.events.add(responseEvent, handler) + } + + public async executeClient< + Args extends any[] = unknown[], + Return = unknown, + >( + player: any, + eventName: string, + ...args: Args + ): Promise { + return new Promise((resolve, reject) => { + const uuid = this._utils.generateUUID() + + const data: RPCState = { + uuid, + eventName, + calledFrom: this._environment, + data: args, + } + + player.call(EVENT_LISTENER, [this._utils.prepareForTransfer(data)]) + + this.handleClientServerReturn(uuid, resolve, reject) + }) + } +} + +export const client = new Client() diff --git a/rpc/src/modules/server.ts b/rpc/src/modules/server.ts new file mode 100644 index 0000000..cda4a55 --- /dev/null +++ b/rpc/src/modules/server.ts @@ -0,0 +1,95 @@ +import { Wrapper } from './wrapper' +import { RPCState, utils } from '../utils' +import { EVENT_LISTENER } from '../events' + +class Server extends Wrapper { + private sendResponseToClient(player: any, data: RPCState) { + const eventName = this._utils.generateResponseEventName(data.uuid) + const preparedData = this._utils.prepareForTransfer(data) + + player.call(eventName, [preparedData]) + } + + public async listenEvent(player: any, data: string) { + const rpcData = this._verifyEvent(data) + + if (rpcData.knownError) { + this._triggerError(rpcData) + return + } + + try { + const fnResponse = await this._state[rpcData.eventName]( + ...rpcData.data, + ) + const response = { + ...rpcData, + data: fnResponse, + } + + this.sendResponseToClient(player, response) + } catch (e) { + this._triggerError(rpcData, e) + } + } + + private handleServerClientReturn( + uuid: string, + resolve: (value: unknown) => void, + reject: (reason?: any) => void, + ) { + const responseEvent = this._utils.generateResponseEventName(uuid) + const timeoutDuration = 1000 * 10 + + const timeoutID = setTimeout(() => { + reject(new Error('Timeout ended')) + mp.events.remove(responseEvent) + }, timeoutDuration) + + const handler = (response: string) => { + const { knownError, data } = this._utils.prepareForExecute(response) + + if (knownError) { + try { + clearTimeout(timeoutID) + reject(knownError) + return + } catch (e) {} + } + + resolve(data) + mp.events.remove(responseEvent) + + try { + clearTimeout(timeoutID) + } catch (e) {} + } + + mp.events.add(responseEvent, handler) + } + + public async executeServer< + Args extends any[] = unknown[], + Return = unknown, + >(eventName: string, ...args: Args): Promise { + return new Promise((resolve, reject) => { + const uuid = this._utils.generateUUID() + + const data: RPCState = { + uuid, + eventName, + calledFrom: this._environment, + data: args, + } + + mp.events.callRemote( + EVENT_LISTENER, + this._utils.prepareForTransfer(data), + ) + + this.handleServerClientReturn(uuid, resolve, reject) + }) + } +} + +export const server = new Server() diff --git a/rpc/src/modules/wrapper.ts b/rpc/src/modules/wrapper.ts new file mode 100644 index 0000000..8b99b9c --- /dev/null +++ b/rpc/src/modules/wrapper.ts @@ -0,0 +1,32 @@ +import { Environment, Errors, RPCState, utils } from '../utils' + +export class Wrapper { + public _utils = utils + public _environment = utils.getEnvironment() + public _state = this._environment === Environment.CEF ? window : global + + public _verifyEvent(data: string): RPCState { + const rpcData = utils.prepareForExecute(data) + + if (!this._state[rpcData.eventName]) { + rpcData.knownError = Errors.EVENT_NOT_REGISTERED + } + + return rpcData + } + + public _triggerError(rpcData: RPCState, error?: any) { + const errorMessage = [ + `${rpcData.knownError}`, + `Caller: ${rpcData.calledFrom}`, + `Receiver: ${this._environment}`, + `Event: ${rpcData.eventName}`, + ] + + if (error) { + errorMessage.push(`Additional Info: ${error}`) + } + + throw new Error(errorMessage.join(' | ')) + } +} diff --git a/rpc/src/types.d.ts b/rpc/src/types.d.ts new file mode 100644 index 0000000..d2cfb72 --- /dev/null +++ b/rpc/src/types.d.ts @@ -0,0 +1,13 @@ +declare const mp: any +declare const console: any + +declare const setTimeout: (fn: Function, time: number) => number +declare const clearTimeout: (id: number) => void + +declare const global: { + [p: string]: (...args: any[]) => unknown +} + +declare const window: { + [p: string]: (...args: any[]) => unknown +} diff --git a/rpc/src/utils.ts b/rpc/src/utils.ts new file mode 100644 index 0000000..f9f4c58 --- /dev/null +++ b/rpc/src/utils.ts @@ -0,0 +1,62 @@ +import { EVENT_RESPONSE } from './events' + +export enum Environment { + CEF = 'CEF', + CLIENT = 'CLIENT', + SERVER = 'SERVER', + UNKNOWN = 'UNKNOWN', +} + +export enum Errors { + EVENT_NOT_REGISTERED = 'Event not registered', +} + +export type RPCState = { + eventName: string + uuid: string + knownError?: string + data?: any + calledFrom: Environment +} + +class Utils { + public getEnvironment(): Environment { + if (mp.joaat) return Environment.SERVER + if (mp.game && mp.game.joaat) return Environment.CLIENT + if ('mp' in window) return Environment.CEF + return Environment.UNKNOWN + } + + public prepareForExecute(data: string): RPCState { + return JSON.parse(data) + } + + public prepareForTransfer(data: RPCState): string { + return JSON.stringify(data) + } + + public generateUUID(): string { + let uuid = '', + random + + for (let i = 0; i < 32; i++) { + random = (Math.random() * 16) | 0 + + if (i === 8 || i === 12 || i === 16 || i === 20) { + uuid += '-' + } + + uuid += ( + i === 12 ? 4 : i === 16 ? (random & 3) | 8 : random + ).toString(16) + } + + return uuid + } + + public generateResponseEventName(uuid: string): string { + return `${EVENT_RESPONSE}_${uuid}` + } +} + +export const utils = new Utils() diff --git a/rpc/tsconfig.json b/rpc/tsconfig.json new file mode 100644 index 0000000..f8996ab --- /dev/null +++ b/rpc/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "es6", + "module": "commonjs", + "moduleResolution": "node", + "lib": ["ES6"], + "declaration": true, + "declarationMap": true, + "sourceMap": true, + + "outDir": "bin", + "esModuleInterop": true, + + "strict": true, + "forceConsistentCasingInFileNames": true, + "noImplicitAny": true + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules" + ] +} \ No newline at end of file diff --git a/rpc/tsup.config.ts b/rpc/tsup.config.ts new file mode 100644 index 0000000..af724fb --- /dev/null +++ b/rpc/tsup.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'tsup' + +export default defineConfig({ + entry: ['src/index.ts'], + outDir: './dist', + format: ['cjs'], + noExternal: ['rage-rpc'], + experimentalDts: true, + splitting: false, + sourcemap: false, + clean: true, +})