From 8029f24e9d2e7b590b997304bf36627bb8344d6f Mon Sep 17 00:00:00 2001 From: Danya H Date: Mon, 28 Sep 2026 00:48:31 +0100 Subject: [PATCH] feat(rpc): rework shared-types + include as mandatory dep --- bun.lock | 6 +- rpc/package.json | 8 +-- rpc/src/core/client.ts | 99 +++++++++++++---------------- rpc/src/core/server.ts | 99 ++++++++++++----------------- rpc/src/core/webview.ts | 77 ++++++++++------------ rpc/src/utils/typing.ts | 47 ++++++++++++++ shared-types/index.d.ts | 58 +++++++++++++++++ shared-types/package.json | 11 +++- shared-types/types/types/index.d.ts | 52 --------------- 9 files changed, 235 insertions(+), 222 deletions(-) create mode 100644 rpc/src/utils/typing.ts create mode 100644 shared-types/index.d.ts delete mode 100644 shared-types/types/types/index.d.ts diff --git a/bun.lock b/bun.lock index 935ca86..681717c 100644 --- a/bun.lock +++ b/bun.lock @@ -12,16 +12,16 @@ "rpc": { "name": "@entityseven/fivem-rpc", "version": "0.1.0", + "dependencies": { + "@entityseven/fivem-rpc-shared-types": "workspace:^", + }, "devDependencies": { - "@entityseven/fivem-rpc-shared-types": "workspace:*", "tsdown": "^0.23.0", }, "peerDependencies": { - "@entityseven/fivem-rpc-shared-types": "^1.0.0", "typescript": ">=5", }, "optionalPeers": [ - "@entityseven/fivem-rpc-shared-types", "typescript", ], }, diff --git a/rpc/package.json b/rpc/package.json index 89bc1ad..6c40f22 100644 --- a/rpc/package.json +++ b/rpc/package.json @@ -48,18 +48,16 @@ "prepublishOnly": "bun run build", "typecheck": "tsc --noEmit" }, + "dependencies": { + "@entityseven/fivem-rpc-shared-types": "workspace:^" + }, "devDependencies": { - "@entityseven/fivem-rpc-shared-types": "workspace:*", "tsdown": "^0.23.0" }, "peerDependencies": { - "@entityseven/fivem-rpc-shared-types": "^1.0.0", "typescript": ">=5" }, "peerDependenciesMeta": { - "@entityseven/fivem-rpc-shared-types": { - "optional": true - }, "typescript": { "optional": true } diff --git a/rpc/src/core/client.ts b/rpc/src/core/client.ts index 244076a..660cf1c 100644 --- a/rpc/src/core/client.ts +++ b/rpc/src/core/client.ts @@ -17,6 +17,13 @@ import { type RPCStateRaw, type RPCStateWeb, } from '../utils/types' +import type { + RPCCommandName, + RPCEventArgs, + RPCEventName, + RPCEventResult, + RPCListener, +} from '../utils/typing' import { RPCInstanceBase } from './base' export class RPCInstanceClient extends RPCInstanceBase { @@ -116,63 +123,53 @@ export class RPCInstanceClient extends RPCInstanceBase { // ===== SERVER ===== - public onServer< - EventName extends keyof s.RPCEvents_ServerClient, - CallbackArguments extends Parameters, - CallbackReturn extends ReturnType, - >( + public onServer>( eventName: EventName, - cb: ( - ...args: CallbackArguments - ) => Awaited | Promise>, + cb: RPCListener, ): this { return this.listen(this._emitterServer, 'onServer', eventName, cb) } - public offServer( + public offServer>( eventName: EventName, ): this { return this.unlisten(this._emitterServer, 'offServer', eventName) } public async emitServer< - EventName extends keyof s.RPCEvents_ClientServer, - Arguments extends Parameters, - Response extends ReturnType, - >(eventName: EventName, ...args: Arguments): Promise> { + EventName extends RPCEventName, + >( + eventName: EventName, + ...args: RPCEventArgs + ): Promise> { const payload = this.request(eventName, 'server', args, null) emitNet(RPCEvents.LISTENER_CLIENT, stringify(payload)) - return this._pending.wait>(payload) + return this._pending.wait(payload) } // ===== WEBVIEW ===== - public onWebview< - EventName extends keyof s.RPCEvents_WebviewClient, - CallbackArguments extends Parameters, - CallbackReturn extends ReturnType, - >( + public onWebview>( eventName: EventName, - cb: ( - ...args: CallbackArguments - ) => Awaited | Promise>, + cb: RPCListener, ): this { return this.listen(this._emitterWeb, 'onWebview', eventName, cb) } - public offWebview( + public offWebview>( eventName: EventName, ): this { return this.unlisten(this._emitterWeb, 'offWebview', eventName) } public async emitWebview< - EventName extends keyof s.RPCEvents_ClientWebview, - Arguments extends Parameters, - Response extends ReturnType, - >(eventName: EventName, ...args: Arguments): Promise> { + EventName extends RPCEventName, + >( + eventName: EventName, + ...args: RPCEventArgs + ): Promise> { const payload = this.request(eventName, 'webview', args, null) this._sendWebMessage({ @@ -180,46 +177,36 @@ export class RPCInstanceClient extends RPCInstanceBase { data: payload, }) - return this._pending.wait>(payload) + return this._pending.wait(payload) } // ===== SELF ===== - public onSelf< - EventName extends keyof s.RPCEvents_Client, - CallbackArguments extends Parameters, - CallbackReturn extends ReturnType, - >( + public onSelf>( eventName: EventName, - cb: ( - ...args: CallbackArguments - ) => Awaited | Promise>, + cb: RPCListener, ): this { return this.listen(this._emitterLocal, 'onSelf', eventName, cb) } - public offSelf( + public offSelf>( eventName: EventName, ): this { return this.unlisten(this._emitterLocal, 'offSelf', eventName) } - public async emitSelf< - EventName extends keyof s.RPCEvents_Client, - Arguments extends Parameters, - Response extends ReturnType, - >(eventName: EventName, ...args: Arguments): Promise> { - return this.emitLocal>(eventName, args) + public async emitSelf>( + eventName: EventName, + ...args: RPCEventArgs + ): Promise> { + return this.emitLocal(eventName, args) } // ===== OTHER ===== - public onCommand< - CommandName extends s.RPCCommands_Client, - CallbackArguments extends unknown[], - >( + public onCommand>( command: CommandName, - cb: (player: number, args: CallbackArguments, commandRaw: string) => void, + cb: (player: number, args: string[], rawCommand: string) => void, ): this { this.log(`onCommand ${command}`) @@ -228,10 +215,10 @@ export class RPCInstanceClient extends RPCInstanceBase { return this } - public onNativeEvent< - EventName extends keyof RPCNativeClientEvents, - CallbackArguments extends Parameters, - >(eventName: EventName, cb: (...args: CallbackArguments) => void): this { + public onNativeEvent( + eventName: EventName, + cb: (...args: Parameters) => void, + ): this { if (!NATIVE_CLIENT_EVENTS.includes(eventName)) { throw new RPCError( RPCErrors.UNKNOWN_NATIVE, @@ -248,10 +235,10 @@ export class RPCInstanceClient extends RPCInstanceBase { public onNativeNetworkEvent< EventName extends keyof RPCNativeClientNetworkEvents, - CallbackArguments extends Parameters< - RPCNativeClientNetworkEvents[EventName] - >, - >(eventName: EventName, cb: (...args: CallbackArguments) => void): this { + >( + eventName: EventName, + cb: (...args: Parameters) => void, + ): this { if (!NATIVE_CLIENT_NETWORK_EVENTS.includes(eventName)) { throw new RPCError( RPCErrors.UNKNOWN_NATIVE, diff --git a/rpc/src/core/server.ts b/rpc/src/core/server.ts index 0e3d76e..1f86baf 100644 --- a/rpc/src/core/server.ts +++ b/rpc/src/core/server.ts @@ -11,6 +11,13 @@ import { type RPCNativeServerEvents, type RPCStateRaw, } from '../utils/types' +import type { + RPCCommandName, + RPCEventArgs, + RPCEventName, + RPCEventResult, + RPCListener, +} from '../utils/typing' import { RPCInstanceBase } from './base' export class RPCInstanceServer extends RPCInstanceBase { @@ -68,46 +75,39 @@ export class RPCInstanceServer extends RPCInstanceBase { // ===== CLIENT ===== - public onClient< - EventName extends keyof s.RPCEvents_ClientServer, - CallbackArguments extends Parameters, - CallbackReturn extends ReturnType, - >( + public onClient>( eventName: EventName, - cb: ( - player: number, - ...args: CallbackArguments - ) => Awaited | Promise>, + cb: RPCListener, ): this { return this.listen(this._emitterClient, 'onClient', eventName, cb) } - public offClient( + public offClient>( eventName: EventName, ): this { return this.unlisten(this._emitterClient, 'offClient', eventName) } public async emitClient< - EventName extends keyof s.RPCEvents_ServerClient, - Arguments extends Parameters, - Response extends ReturnType, + EventName extends RPCEventName, >( player: number, eventName: EventName, - ...args: Arguments - ): Promise> { + ...args: RPCEventArgs + ): Promise> { const payload = this.request(eventName, 'client', args, player) emitNet(RPCEvents.LISTENER_SERVER, player, stringify(payload)) - return this._pending.wait>(payload, player) + return this._pending.wait(payload, player) } public async emitClientEveryone< - EventName extends keyof s.RPCEvents_ServerClient, - Arguments extends Parameters, - >(eventName: EventName, ...args: Arguments): Promise { + EventName extends RPCEventName, + >( + eventName: EventName, + ...args: RPCEventArgs + ): Promise { const payload = this.request(eventName, 'client', args, -1, 'broadcast') emitNet(RPCEvents.LISTENER_SERVER, -1, stringify(payload)) @@ -115,79 +115,60 @@ export class RPCInstanceServer extends RPCInstanceBase { // ===== WEBVIEW ===== - public onWebview< - EventName extends keyof s.RPCEvents_WebviewServer, - CallbackArguments extends Parameters, - CallbackReturn extends ReturnType, - >( + public onWebview>( eventName: EventName, - cb: ( - player: number, - ...args: CallbackArguments - ) => Awaited | Promise>, + cb: RPCListener, ): this { return this.listen(this._emitterWeb, 'onWebview', eventName, cb) } - public offWebview( + public offWebview>( eventName: EventName, ): this { return this.unlisten(this._emitterWeb, 'offWebview', eventName) } public async emitWebview< - EventName extends keyof s.RPCEvents_ServerWebview, - Arguments extends Parameters, - Response extends ReturnType, + EventName extends RPCEventName, >( player: number, eventName: EventName, - ...args: Arguments - ): Promise> { + ...args: RPCEventArgs + ): Promise> { const payload = this.request(eventName, 'webview', args, player) emitNet(RPCEvents.LISTENER_SERVER, player, stringify(payload)) - return this._pending.wait>(payload, player) + return this._pending.wait(payload, player) } // ===== SELF ===== - public onSelf< - EventName extends keyof s.RPCEvents_Server, - CallbackArguments extends Parameters, - CallbackReturn extends ReturnType, - >( + public onSelf>( eventName: EventName, - cb: ( - ...args: CallbackArguments - ) => Awaited | Promise>, + cb: RPCListener, ): this { return this.listen(this._emitterLocal, 'onSelf', eventName, cb) } - public offSelf( + public offSelf>( eventName: EventName, ): this { return this.unlisten(this._emitterLocal, 'offSelf', eventName) } - public async emitSelf< - EventName extends keyof s.RPCEvents_Server, - Arguments extends Parameters, - Response extends ReturnType, - >(eventName: EventName, ...args: Arguments): Promise> { - return this.emitLocal>(eventName, args) + public async emitSelf>( + eventName: EventName, + ...args: RPCEventArgs + ): Promise> { + return this.emitLocal(eventName, args) } // ===== OTHER ===== - public onCommand< - CommandName extends s.RPCCommands_Server, - CallbackArguments extends unknown[], - >( + public onCommand>( command: CommandName, - cb: (player: number, args: CallbackArguments, commandRaw: string) => void, + cb: (player: number, args: string[], rawCommand: string) => void, restricted = false, ): this { this.log(`onCommand ${command}`) @@ -197,10 +178,10 @@ export class RPCInstanceServer extends RPCInstanceBase { return this } - public onNativeEvent< - EventName extends keyof RPCNativeServerEvents, - CallbackArguments extends Parameters, - >(eventName: EventName, cb: (...args: CallbackArguments) => void): this { + public onNativeEvent( + eventName: EventName, + cb: (...args: Parameters) => void, + ): this { if (!NATIVE_SERVER_EVENTS.includes(eventName)) { throw new RPCError( RPCErrors.UNKNOWN_NATIVE, diff --git a/rpc/src/core/webview.ts b/rpc/src/core/webview.ts index 10ebeb2..ec5e4e1 100644 --- a/rpc/src/core/webview.ts +++ b/rpc/src/core/webview.ts @@ -9,6 +9,12 @@ import { type RPCStateRaw, type RPCStateWeb, } from '../utils/types' +import type { + RPCEventArgs, + RPCEventName, + RPCEventResult, + RPCListener, +} from '../utils/typing' import { RPCInstanceBase } from './base' export class RPCInstanceWebview extends RPCInstanceBase { @@ -64,93 +70,76 @@ export class RPCInstanceWebview extends RPCInstanceBase { // ===== CLIENT ===== - public onClient< - EventName extends keyof s.RPCEvents_ClientWebview, - CallbackArguments extends Parameters, - CallbackReturn extends ReturnType, - >( + public onClient>( eventName: EventName, - cb: ( - ...args: CallbackArguments - ) => Awaited | Promise>, + cb: RPCListener, ): this { return this.listen(this._emitterClient, 'onClient', eventName, cb) } - public offClient( + public offClient>( eventName: EventName, ): this { return this.unlisten(this._emitterClient, 'offClient', eventName) } public async emitClient< - EventName extends keyof s.RPCEvents_WebviewClient, - Arguments extends Parameters, - Response extends ReturnType, - >(eventName: EventName, ...args: Arguments): Promise> { + EventName extends RPCEventName, + >( + eventName: EventName, + ...args: RPCEventArgs + ): Promise> { const payload = this.request(eventName, 'client', args, null) - return this._request>(payload) + return this._request(payload) } // ===== SERVER ===== - public onServer< - EventName extends keyof s.RPCEvents_ServerWebview, - CallbackArguments extends Parameters, - CallbackReturn extends ReturnType, - >( + public onServer>( eventName: EventName, - cb: ( - ...args: CallbackArguments - ) => Awaited | Promise>, + cb: RPCListener, ): this { return this.listen(this._emitterServer, 'onServer', eventName, cb) } - public offServer( + public offServer>( eventName: EventName, - ): RPCInstanceWebview { + ): this { return this.unlisten(this._emitterServer, 'offServer', eventName) } public async emitServer< - EventName extends keyof s.RPCEvents_WebviewServer, - Arguments extends Parameters, - Response extends ReturnType, - >(eventName: EventName, ...args: Arguments): Promise> { + EventName extends RPCEventName, + >( + eventName: EventName, + ...args: RPCEventArgs + ): Promise> { const payload = this.request(eventName, 'server', args, null) - return this._request>(payload) + return this._request(payload) } // ===== SELF ===== - public onSelf< - EventName extends keyof s.RPCEvents_Webview, - CallbackArguments extends Parameters, - CallbackReturn extends ReturnType, - >( + public onSelf>( eventName: EventName, - cb: ( - ...args: CallbackArguments - ) => Awaited | Promise>, + cb: RPCListener, ): this { return this.listen(this._emitterLocal, 'onSelf', eventName, cb) } - public offSelf( + public offSelf>( eventName: EventName, ): this { return this.unlisten(this._emitterLocal, 'offSelf', eventName) } - public async emitSelf< - EventName extends keyof s.RPCEvents_Webview, - Arguments extends Parameters, - Response extends ReturnType, - >(eventName: EventName, ...args: Arguments): Promise> { - return this.emitLocal>(eventName, args) + public async emitSelf>( + eventName: EventName, + ...args: RPCEventArgs + ): Promise> { + return this.emitLocal(eventName, args) } // ===== UTILS ===== diff --git a/rpc/src/utils/typing.ts b/rpc/src/utils/typing.ts new file mode 100644 index 0000000..1dc306e --- /dev/null +++ b/rpc/src/utils/typing.ts @@ -0,0 +1,47 @@ +/** + * **Internal** Typing helpers over the event maps of + * `@entityseven/fivem-rpc-shared-types`. A map with declarations is typed + * strictly; an empty map (nothing declared, or the package not installed) + * accepts any name, any arguments and any result. + */ + +// oxlint-disable-next-line typescript/no-explicit-any -- loose mode must accept listeners with any parameter types +type AnyEvent = (...args: any[]) => any + +/** `true` for `any`, which is what the maps become if the package is missing */ +type IsAny = 0 extends 1 & T ? true : false + +/** The declared map, or a loose one when nothing is declared */ +type EventMap = + IsAny extends true + ? Record + : [keyof T] extends [never] + ? Record + : T + +export type RPCEventName = keyof EventMap & string + +export type RPCEventArgs< + T, + K extends RPCEventName, +> = EventMap[K] extends (...args: infer A extends unknown[]) => unknown + ? A + : never + +export type RPCEventResult< + T, + K extends RPCEventName, +> = EventMap[K] extends (...args: never[]) => infer R ? Awaited : never + +/** Listener for event `K` of map `T`; `Prefix` goes before the event arguments */ +export type RPCListener< + T, + K extends RPCEventName, + Prefix extends unknown[] = [], +> = ( + ...args: [...Prefix, ...RPCEventArgs] +) => RPCEventResult | Promise> + +export type RPCCommandName = [keyof T] extends [never] + ? string + : keyof T & string diff --git a/shared-types/index.d.ts b/shared-types/index.d.ts new file mode 100644 index 0000000..549d315 --- /dev/null +++ b/shared-types/index.d.ts @@ -0,0 +1,58 @@ +/** + * Event and command declarations for `@entityseven/fivem-rpc`. + * + * Every interface starts empty: until you declare something in it, the matching + * rpc methods accept any name and any arguments. Declare your own by augmenting + * this module from any `.d.ts` (or `.ts`) file included in your project: + * + * @example + * // shared/rpc.d.ts + * import '@entityseven/fivem-rpc-shared-types' + * + * declare module '@entityseven/fivem-rpc-shared-types' { + * interface RPCEvents_ClientServer { + * // name(arguments): value returned by the listener + * buyItem(item: string, amount: number): boolean + * } + * interface RPCCommands_Server { + * ban: true + * } + * } + */ + +// ===== COMMANDS (names are the keys, values are not used) ===== + +/** Commands registered with `rpc.onCommand` on the client */ +export interface RPCCommands_Client {} + +/** Commands registered with `rpc.onCommand` on the server */ +export interface RPCCommands_Server {} + +// ===== EVENTS (caller -> receiver) ===== + +/** Client -> client: `emitSelf` / `onSelf` on the client */ +export interface RPCEvents_Client {} + +/** Client -> server: `emitServer` on the client, `onClient` on the server */ +export interface RPCEvents_ClientServer {} + +/** Client -> webview: `emitWebview` on the client, `onClient` on the webview */ +export interface RPCEvents_ClientWebview {} + +/** Server -> server: `emitSelf` / `onSelf` on the server */ +export interface RPCEvents_Server {} + +/** Server -> client: `emitClient` on the server, `onServer` on the client */ +export interface RPCEvents_ServerClient {} + +/** Server -> webview: `emitWebview` on the server, `onServer` on the webview */ +export interface RPCEvents_ServerWebview {} + +/** Webview -> webview: `emitSelf` / `onSelf` on the webview */ +export interface RPCEvents_Webview {} + +/** Webview -> client: `emitClient` on the webview, `onWebview` on the client */ +export interface RPCEvents_WebviewClient {} + +/** Webview -> server: `emitServer` on the webview, `onWebview` on the server */ +export interface RPCEvents_WebviewServer {} diff --git a/shared-types/package.json b/shared-types/package.json index 3027291..cada3f2 100644 --- a/shared-types/package.json +++ b/shared-types/package.json @@ -28,12 +28,17 @@ "directory": "shared-types" }, "files": [ - "types/**/*", + "index.d.ts", "readme.md", "license.md" ], - "type": "module", - "types": "types/types/index.d.ts", + "types": "./index.d.ts", + "exports": { + ".": { + "types": "./index.d.ts" + }, + "./package.json": "./package.json" + }, "peerDependencies": { "typescript": ">=5" }, diff --git a/shared-types/types/types/index.d.ts b/shared-types/types/types/index.d.ts deleted file mode 100644 index 4c2634d..0000000 --- a/shared-types/types/types/index.d.ts +++ /dev/null @@ -1,52 +0,0 @@ -declare module '@entityseven/fivem-rpc-shared-types' { - // Client commands names - export type RPCCommands_Client = '' - - // Server commands names - export type RPCCommands_Server = '' - - // Client -> Client events - export interface RPCEvents_Client { - _(): void - } - - // Client -> Server events - export interface RPCEvents_ClientServer { - _(): void - } - - // Client -> Webview events - export interface RPCEvents_ClientWebview { - _(): void - } - - // Server -> Server events - export interface RPCEvents_Server { - _(): void - } - - // Server -> Client events - export interface RPCEvents_ServerClient { - _(): void - } - - // Server -> Server events - export interface RPCEvents_ServerWebview { - _(): void - } - - // Webview -> Webview events - export interface RPCEvents_Webview { - _(): void - } - - // Webview -> Client events - export interface RPCEvents_WebviewClient { - _(): void - } - - // Webview -> Server events - export interface RPCEvents_WebviewServer { - _(): void - } -}