From cca07468d5d1748152a452c584b5df71a93fcd83 Mon Sep 17 00:00:00 2001 From: Danya H Date: Mon, 28 Sep 2026 01:47:46 +0100 Subject: [PATCH] docs: TSdoc --- .oxlintrc.json | 12 ++++- rpc/src/core/base.ts | 4 ++ rpc/src/core/client.ts | 111 ++++++++++++++++++++++++++++++++++++++++ rpc/src/core/server.ts | 108 +++++++++++++++++++++++++++++++++++++- rpc/src/core/webview.ts | 75 +++++++++++++++++++++++++++ rpc/src/index.ts | 4 ++ rpc/src/utils/native.ts | 11 +++- rpc/src/utils/types.ts | 20 ++++++-- shared-types/readme.md | 2 +- 9 files changed, 336 insertions(+), 11 deletions(-) diff --git a/.oxlintrc.json b/.oxlintrc.json index ac7f8d8..6650c0c 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -1,6 +1,6 @@ { "$schema": "./node_modules/oxlint/configuration_schema.json", - "plugins": ["eslint", "typescript", "unicorn", "oxc", "import"], + "plugins": ["eslint", "typescript", "unicorn", "oxc", "import", "jsdoc"], "categories": { "correctness": "error", "suspicious": "warn" @@ -8,7 +8,15 @@ "rules": { "typescript/no-explicit-any": "error", "typescript/no-unsafe-function-type": "error", - "eslint/no-underscore-dangle": "off" + "eslint/no-underscore-dangle": "off", + "jsdoc/check-tag-names": [ + "error", + { "typed": true, "definedTags": ["defaultValue", "remarks", "typeParam"] } + ], + "jsdoc/empty-tags": "error", + "jsdoc/no-blank-blocks": "error", + "jsdoc/require-param-description": "error", + "jsdoc/require-param-name": "error" }, "ignorePatterns": ["**/dist"] } diff --git a/rpc/src/core/base.ts b/rpc/src/core/base.ts index 8d15bcc..1f34c94 100644 --- a/rpc/src/core/base.ts +++ b/rpc/src/core/base.ts @@ -14,6 +14,10 @@ import { type RPCStateRaw, } from '../utils/types' +/** + * **Internal** Shared by `RPCInstanceServer`, `RPCInstanceClient` and + * `RPCInstanceWebview`. + */ export class RPCInstanceBase { protected readonly env: RPCEnvironment protected readonly debug: boolean diff --git a/rpc/src/core/client.ts b/rpc/src/core/client.ts index 660cf1c..852a4c9 100644 --- a/rpc/src/core/client.ts +++ b/rpc/src/core/client.ts @@ -26,6 +26,18 @@ import type { } from '../utils/typing' import { RPCInstanceBase } from './base' +/** + * RPC instance for client code, returned by `createRPC({ env: 'client' })`. + * Create one per client and import it from your own module. + * + * - `on*` registers the listener that answers calls from one direction. One + * listener per event: registering the same name again replaces it, `off*` + * removes it + * - `emit*` calls the listener on the target and resolves with its return + * value, or rejects with {@link RPCError} + * - also relays calls between its webview and the server, so every client + * needs an instance even without listeners of its own + */ export class RPCInstanceClient extends RPCInstanceBase { private readonly _emitterServer: Emitter private readonly _emitterWeb: Emitter @@ -123,6 +135,16 @@ export class RPCInstanceClient extends RPCInstanceBase { // ===== SERVER ===== + /** + * Listens for `emitClient` and `emitClientEveryone` calls from the server + * (server -> client). For `emitClientEveryone` the return value is not sent. + * + * @param cb - gets the event arguments. Its return value (awaited) is sent + * back to the caller + * + * @example + * rpc.onServer('askTrade', offer => showTradeDialog(offer)) + */ public onServer>( eventName: EventName, cb: RPCListener, @@ -130,12 +152,23 @@ export class RPCInstanceClient extends RPCInstanceBase { return this.listen(this._emitterServer, 'onServer', eventName, cb) } + /** Removes the `onServer` listener for `eventName` */ public offServer>( eventName: EventName, ): this { return this.unlisten(this._emitterServer, 'offServer', eventName) } + /** + * Calls the server's `onClient` listener (client -> server) and resolves + * with its return value. The server listener gets this player's id first. + * + * @throws {@link RPCError} `EVENT_NOT_REGISTERED` (no listener), + * `HANDLER_ERROR` (the listener threw) or `TIMEOUT` + * + * @example + * const money = await rpc.emitServer('getMoney', 'bank') + */ public async emitServer< EventName extends RPCEventName, >( @@ -151,6 +184,16 @@ export class RPCInstanceClient extends RPCInstanceBase { // ===== WEBVIEW ===== + /** + * Listens for `emitClient` calls from this player's webview + * (webview -> client). + * + * @param cb - gets the event arguments. Its return value (awaited) is sent + * back to the caller + * + * @example + * rpc.onWebview('getPosition', () => GetEntityCoords(PlayerPedId(), false)) + */ public onWebview>( eventName: EventName, cb: RPCListener, @@ -158,12 +201,23 @@ export class RPCInstanceClient extends RPCInstanceBase { return this.listen(this._emitterWeb, 'onWebview', eventName, cb) } + /** Removes the `onWebview` listener for `eventName` */ public offWebview>( eventName: EventName, ): this { return this.unlisten(this._emitterWeb, 'offWebview', eventName) } + /** + * Calls the `onClient` listener in this player's webview (client -> webview) + * and resolves with its return value. + * + * @throws {@link RPCError} `EVENT_NOT_REGISTERED` (no listener), + * `HANDLER_ERROR` (the listener threw) or `TIMEOUT` + * + * @example + * const choice = await rpc.emitWebview('openMenu', items) + */ public async emitWebview< EventName extends RPCEventName, >( @@ -182,6 +236,15 @@ export class RPCInstanceClient extends RPCInstanceBase { // ===== SELF ===== + /** + * Listens for `emitSelf` calls in this environment (client -> client). + * + * @param cb - gets the event arguments. Its return value (awaited) is sent + * back to the caller + * + * @example + * rpc.onSelf('add', (a, b) => a + b) + */ public onSelf>( eventName: EventName, cb: RPCListener, @@ -189,12 +252,23 @@ export class RPCInstanceClient extends RPCInstanceBase { return this.listen(this._emitterLocal, 'onSelf', eventName, cb) } + /** Removes the `onSelf` listener for `eventName` */ public offSelf>( eventName: EventName, ): this { return this.unlisten(this._emitterLocal, 'offSelf', eventName) } + /** + * Calls this environment's own `onSelf` listener directly and resolves with + * its return value. No timeout; errors thrown by the listener reach the caller + * unchanged. + * + * @throws {@link RPCError} `EVENT_NOT_REGISTERED` if no `onSelf` listener exists + * + * @example + * const total = await rpc.emitSelf('add', 2, 3) + */ public async emitSelf>( eventName: EventName, ...args: RPCEventArgs @@ -204,6 +278,15 @@ export class RPCInstanceClient extends RPCInstanceBase { // ===== OTHER ===== + /** + * Registers a chat command (FiveM `RegisterCommand`). + * + * @param cb - gets FiveM's `source`, the strings typed after the command and + * the full command line. Validate `args` yourself + * + * @example + * rpc.onCommand('coords', () => console.log(GetEntityCoords(PlayerPedId(), false))) + */ public onCommand>( command: CommandName, cb: (player: number, args: string[], rawCommand: string) => void, @@ -215,6 +298,15 @@ export class RPCInstanceClient extends RPCInstanceBase { return this } + /** + * Listens to a native FiveM client event, e.g. `entityDamaged`. + * + * @throws {@link RPCError} `UNKNOWN_NATIVE` if `eventName` is not in + * `NATIVE_CLIENT_EVENTS`. Register other events with FiveM's `on` directly + * + * @example + * rpc.onNativeEvent('entityDamaged', (victim, culprit) => console.log(victim)) + */ public onNativeEvent( eventName: EventName, cb: (...args: Parameters) => void, @@ -233,6 +325,16 @@ export class RPCInstanceClient extends RPCInstanceBase { return this } + /** + * Listens to a native game event, e.g. `CEventShockingCarCrash`. + * + * @throws {@link RPCError} `UNKNOWN_NATIVE` if `eventName` is not in + * `NATIVE_CLIENT_NETWORK_EVENTS`. Register other events with FiveM's `on` + * directly + * + * @example + * rpc.onNativeNetworkEvent('CEventShockingCarCrash', (entities, eventEntity) => {}) + */ public onNativeNetworkEvent< EventName extends keyof RPCNativeClientNetworkEvents, >( @@ -253,6 +355,15 @@ export class RPCInstanceClient extends RPCInstanceBase { return this } + /** + * Focuses this player's webview (FiveM `SetNuiFocus`). + * + * @param hasFocus - the webview receives keyboard input + * @param hasCursor - the mouse cursor is shown + * + * @example + * rpc.setWebviewFocus(true, true) + */ public setWebviewFocus(hasFocus: boolean, hasCursor: boolean): this { this.log(`setWebviewFocus ${hasFocus} ${hasCursor}`) diff --git a/rpc/src/core/server.ts b/rpc/src/core/server.ts index 1f86baf..7a7d550 100644 --- a/rpc/src/core/server.ts +++ b/rpc/src/core/server.ts @@ -20,6 +20,18 @@ import type { } from '../utils/typing' import { RPCInstanceBase } from './base' +/** + * RPC instance for server code, returned by `createRPC({ env: 'server' })`. + * Create one per server and import it from your own module. + * + * - `on*` registers the listener that answers calls from one direction. One + * listener per event: registering the same name again replaces it, `off*` + * removes it + * - `emit*` calls the listener on the target and resolves with its return + * value, or rejects with {@link RPCError} + * - listeners for client and webview calls get the calling player's server id + * first, taken from FiveM `source` + */ export class RPCInstanceServer extends RPCInstanceBase { private readonly _emitterClient: Emitter private readonly _emitterWeb: Emitter @@ -75,6 +87,16 @@ export class RPCInstanceServer extends RPCInstanceBase { // ===== CLIENT ===== + /** + * Listens for `emitServer` calls from clients (client -> server). + * + * @param cb - gets the calling player's server id first (from FiveM + * `source`, never from the payload), then the event arguments. Its return + * value (awaited) is sent back to the caller + * + * @example + * rpc.onClient('getMoney', (player, account) => getMoney(player, account)) + */ public onClient>( eventName: EventName, cb: RPCListener, @@ -82,12 +104,24 @@ export class RPCInstanceServer extends RPCInstanceBase { return this.listen(this._emitterClient, 'onClient', eventName, cb) } + /** Removes the `onClient` listener for `eventName` */ public offClient>( eventName: EventName, ): this { return this.unlisten(this._emitterClient, 'offClient', eventName) } + /** + * Calls the `onServer` listener on one client (server -> client) and + * resolves with its return value. + * + * @param player - server id of the target player + * @throws {@link RPCError} `EVENT_NOT_REGISTERED` (no listener), + * `HANDLER_ERROR` (the listener threw) or `TIMEOUT` + * + * @example + * const accepted = await rpc.emitClient(player, 'askTrade', offer) + */ public async emitClient< EventName extends RPCEventName, >( @@ -102,6 +136,14 @@ export class RPCInstanceServer extends RPCInstanceBase { return this._pending.wait(payload, player) } + /** + * Runs the `onServer` listener on every client (server -> all clients). + * One-way: resolves once sent, clients do not answer and their failures stay + * on the client. + * + * @example + * await rpc.emitClientEveryone('weatherChanged', 'RAIN') + */ public async emitClientEveryone< EventName extends RPCEventName, >( @@ -115,6 +157,17 @@ export class RPCInstanceServer extends RPCInstanceBase { // ===== WEBVIEW ===== + /** + * Listens for `emitServer` calls from webviews (webview -> server, relayed + * by the player's client). + * + * @param cb - gets the calling player's server id first (from FiveM + * `source`, never from the payload), then the event arguments. Its return + * value (awaited) is sent back to the caller + * + * @example + * rpc.onWebview('buyItem', (player, item) => shop.buy(player, item)) + */ public onWebview>( eventName: EventName, cb: RPCListener, @@ -122,12 +175,24 @@ export class RPCInstanceServer extends RPCInstanceBase { return this.listen(this._emitterWeb, 'onWebview', eventName, cb) } + /** Removes the `onWebview` listener for `eventName` */ public offWebview>( eventName: EventName, ): this { return this.unlisten(this._emitterWeb, 'offWebview', eventName) } + /** + * Calls the `onServer` listener in one player's webview (server -> webview, + * relayed by that player's client) and resolves with its return value. + * + * @param player - server id of the target player + * @throws {@link RPCError} `EVENT_NOT_REGISTERED` (no listener), + * `HANDLER_ERROR` (the listener threw) or `TIMEOUT` + * + * @example + * const confirmed = await rpc.emitWebview(player, 'confirmPurchase', item) + */ public async emitWebview< EventName extends RPCEventName, >( @@ -144,19 +209,39 @@ export class RPCInstanceServer extends RPCInstanceBase { // ===== SELF ===== + /** + * Listens for `emitSelf` calls in this environment (server -> server). + * + * @param cb - gets the event arguments. Its return value (awaited) is sent + * back to the caller + * + * @example + * rpc.onSelf('add', (a, b) => a + b) + */ public onSelf>( eventName: EventName, - cb: RPCListener, + cb: RPCListener, ): this { return this.listen(this._emitterLocal, 'onSelf', eventName, cb) } + /** Removes the `onSelf` listener for `eventName` */ public offSelf>( eventName: EventName, ): this { return this.unlisten(this._emitterLocal, 'offSelf', eventName) } + /** + * Calls this environment's own `onSelf` listener directly and resolves with + * its return value. No timeout; errors thrown by the listener reach the caller + * unchanged. + * + * @throws {@link RPCError} `EVENT_NOT_REGISTERED` if no `onSelf` listener exists + * + * @example + * const total = await rpc.emitSelf('add', 2, 3) + */ public async emitSelf>( eventName: EventName, ...args: RPCEventArgs @@ -166,6 +251,18 @@ export class RPCInstanceServer extends RPCInstanceBase { // ===== OTHER ===== + /** + * Registers a chat command (FiveM `RegisterCommand`). + * + * @param cb - gets the player's server id (`0` for the server console), the + * strings typed after the command and the full command line. Validate + * `args` yourself + * @param restricted - only players with the ACE permission `command.` + * can use it + * + * @example + * rpc.onCommand('heal', (player, args) => heal(player, Number(args[0])), true) + */ public onCommand>( command: CommandName, cb: (player: number, args: string[], rawCommand: string) => void, @@ -178,6 +275,15 @@ export class RPCInstanceServer extends RPCInstanceBase { return this } + /** + * Listens to a native FiveM server event, e.g. `playerJoining`. + * + * @throws {@link RPCError} `UNKNOWN_NATIVE` if `eventName` is not in + * `NATIVE_SERVER_EVENTS`. Register other events with FiveM's `on` directly + * + * @example + * rpc.onNativeEvent('playerJoining', (source, oldId) => console.log(source)) + */ public onNativeEvent( eventName: EventName, cb: (...args: Parameters) => void, diff --git a/rpc/src/core/webview.ts b/rpc/src/core/webview.ts index 17836e1..39d4608 100644 --- a/rpc/src/core/webview.ts +++ b/rpc/src/core/webview.ts @@ -16,6 +16,18 @@ import type { } from '../utils/typing' import { RPCInstanceBase } from './base' +/** + * RPC instance for webview code, returned by `createRPC({ env: 'webview' })`. + * Create one per webview and import it from your own module. + * + * - `on*` registers the listener that answers calls from one direction. One + * listener per event: registering the same name again replaces it, `off*` + * removes it + * - `emit*` calls the listener on the target and resolves with its return + * value, or rejects with {@link RPCError} + * - calls to and from the server are relayed by the player's client, which + * must run `createRPC({ env: 'client' })` + */ export class RPCInstanceWebview extends RPCInstanceBase { private readonly _emitterClient: Emitter private readonly _emitterServer: Emitter @@ -69,6 +81,16 @@ export class RPCInstanceWebview extends RPCInstanceBase { // ===== CLIENT ===== + /** + * Listens for `emitWebview` calls from this player's client + * (client -> webview). + * + * @param cb - gets the event arguments. Its return value (awaited) is sent + * back to the caller + * + * @example + * rpc.onClient('openMenu', items => menu.open(items)) + */ public onClient>( eventName: EventName, cb: RPCListener, @@ -76,12 +98,23 @@ export class RPCInstanceWebview extends RPCInstanceBase { return this.listen(this._emitterClient, 'onClient', eventName, cb) } + /** Removes the `onClient` listener for `eventName` */ public offClient>( eventName: EventName, ): this { return this.unlisten(this._emitterClient, 'offClient', eventName) } + /** + * Calls the client's `onWebview` listener (webview -> client) and resolves + * with its return value. + * + * @throws {@link RPCError} `EVENT_NOT_REGISTERED` (no listener), + * `HANDLER_ERROR` (the listener threw) or `TIMEOUT` + * + * @example + * const position = await rpc.emitClient('getPosition') + */ public async emitClient< EventName extends RPCEventName, >( @@ -95,6 +128,16 @@ export class RPCInstanceWebview extends RPCInstanceBase { // ===== SERVER ===== + /** + * Listens for `emitWebview` calls from the server (server -> webview, + * relayed by the client). + * + * @param cb - gets the event arguments. Its return value (awaited) is sent + * back to the caller + * + * @example + * rpc.onServer('confirmPurchase', item => window.confirm(`Buy ${item}?`)) + */ public onServer>( eventName: EventName, cb: RPCListener, @@ -102,12 +145,24 @@ export class RPCInstanceWebview extends RPCInstanceBase { return this.listen(this._emitterServer, 'onServer', eventName, cb) } + /** Removes the `onServer` listener for `eventName` */ public offServer>( eventName: EventName, ): this { return this.unlisten(this._emitterServer, 'offServer', eventName) } + /** + * Calls the server's `onWebview` listener (webview -> server, relayed by + * the client) and resolves with its return value. The server listener gets + * this player's id first. + * + * @throws {@link RPCError} `EVENT_NOT_REGISTERED` (no listener), + * `HANDLER_ERROR` (the listener threw) or `TIMEOUT` + * + * @example + * const bought = await rpc.emitServer('buyItem', 'water') + */ public async emitServer< EventName extends RPCEventName, >( @@ -121,6 +176,15 @@ export class RPCInstanceWebview extends RPCInstanceBase { // ===== SELF ===== + /** + * Listens for `emitSelf` calls in this environment (webview -> webview). + * + * @param cb - gets the event arguments. Its return value (awaited) is sent + * back to the caller + * + * @example + * rpc.onSelf('add', (a, b) => a + b) + */ public onSelf>( eventName: EventName, cb: RPCListener, @@ -128,12 +192,23 @@ export class RPCInstanceWebview extends RPCInstanceBase { return this.listen(this._emitterLocal, 'onSelf', eventName, cb) } + /** Removes the `onSelf` listener for `eventName` */ public offSelf>( eventName: EventName, ): this { return this.unlisten(this._emitterLocal, 'offSelf', eventName) } + /** + * Calls this environment's own `onSelf` listener directly and resolves with + * its return value. No timeout; errors thrown by the listener reach the caller + * unchanged. + * + * @throws {@link RPCError} `EVENT_NOT_REGISTERED` if no `onSelf` listener exists + * + * @example + * const total = await rpc.emitSelf('add', 2, 3) + */ public async emitSelf>( eventName: EventName, ...args: RPCEventArgs diff --git a/rpc/src/index.ts b/rpc/src/index.ts index 180f773..46612d3 100644 --- a/rpc/src/index.ts +++ b/rpc/src/index.ts @@ -12,7 +12,11 @@ import { /** * Creates the RPC instance for one environment. Create exactly one per * environment (server, client, webview) and export it from a local module. + * Every client needs one, even without listeners: it relays calls between + * its webview and the server. * + * @returns `RPCInstanceServer`, `RPCInstanceClient` or `RPCInstanceWebview`, + * matching `config.env` * @throws {@link RPCError} `UNKNOWN_ENVIRONMENT` if `config.env` is not * `'server'`, `'client'` or `'webview'` * diff --git a/rpc/src/utils/native.ts b/rpc/src/utils/native.ts index 349c354..61dd821 100644 --- a/rpc/src/utils/native.ts +++ b/rpc/src/utils/native.ts @@ -23,7 +23,10 @@ const serverEvents = { weaponDamageEvent: true, } satisfies Record -/** https://docs.fivem.net/docs/scripting-reference/events/server-events/ */ +/** + * Events `onNativeEvent` accepts on the server: + * https://docs.fivem.net/docs/scripting-reference/events/server-events/ + */ export const NATIVE_SERVER_EVENTS = Object.keys( serverEvents, ) as readonly (keyof RPCNativeServerEvents)[] @@ -41,12 +44,16 @@ const clientEvents = { populationPedCreating: true, } satisfies Record -/** https://docs.fivem.net/docs/scripting-reference/events/client-events/ */ +/** + * Events `onNativeEvent` accepts on the client: + * https://docs.fivem.net/docs/scripting-reference/events/client-events/ + */ export const NATIVE_CLIENT_EVENTS = Object.keys( clientEvents, ) as readonly (keyof RPCNativeClientEvents)[] /** + * Events `onNativeNetworkEvent` accepts on the client: * https://docs.fivem.net/docs/game-references/game-events/ * * Source of truth for `RPCNativeClientNetworkEventsNames`. diff --git a/rpc/src/utils/types.ts b/rpc/src/utils/types.ts index b42cecb..b7b5d1d 100644 --- a/rpc/src/utils/types.ts +++ b/rpc/src/utils/types.ts @@ -4,7 +4,8 @@ import type { RPCInstanceWebview } from '../core/webview' import type { NATIVE_CLIENT_NETWORK_EVENTS } from './native' /** - * Possible environment states for `RPCConfig` + * Where an instance runs: `server` (server scripts), `client` (client + * scripts) or `webview` (NUI page) */ export type RPCEnvironment = 'server' | 'client' | 'webview' @@ -99,18 +100,22 @@ export enum RPCEvents { LISTENER_WEB = '__rpc:listenerWeb', } -/** - * Errors to check against - */ +/** Values of `RPCError.code` */ export enum RPCErrors { + /** The target has no listener for the event (or `emitSelf` has no `onSelf`) */ EVENT_NOT_REGISTERED = 'Event not registered', + /** `onNative*` got an event that is not in its `NATIVE_*` list */ UNKNOWN_NATIVE = 'Unknown native event', + /** `createRPC` got an `env` other than server, client or webview */ UNKNOWN_ENVIRONMENT = 'Unknown environment', + /** No response within `RPCConfig.timeout` */ TIMEOUT = 'Timed out waiting for response', + /** The listener on the target threw; the message carries its error */ HANDLER_ERROR = 'Listener threw an error', } /** + * Native server events accepted by `onNativeEvent` on the server: * https://docs.fivem.net/docs/scripting-reference/events/server-events/ */ export type RPCNativeServerEvents = { @@ -245,6 +250,7 @@ export type RPCNativeServerEvents = { } /** + * Native client events accepted by `onNativeEvent` on the client: * https://docs.fivem.net/docs/scripting-reference/events/client-events/ */ export type RPCNativeClientEvents = { @@ -277,6 +283,7 @@ export type RPCNativeClientEvents = { ): void } +/** Game events accepted by `onNativeNetworkEvent` on the client */ export type RPCNativeClientNetworkEvents = { [name in RPCNativeClientNetworkEventsNames]: ( entities: number[], @@ -285,6 +292,9 @@ export type RPCNativeClientNetworkEvents = { ) => void } -/** https://docs.fivem.net/docs/game-references/game-events/ */ +/** + * Names in `NATIVE_CLIENT_NETWORK_EVENTS`: + * https://docs.fivem.net/docs/game-references/game-events/ + */ export type RPCNativeClientNetworkEventsNames = (typeof NATIVE_CLIENT_NETWORK_EVENTS)[number] diff --git a/shared-types/readme.md b/shared-types/readme.md index 6061261..8913a69 100644 --- a/shared-types/readme.md +++ b/shared-types/readme.md @@ -67,7 +67,7 @@ Each interface is one direction. An interface you leave empty stays loose (any n | `RPCCommands_Client` | - | - | `onCommand` (client) | | `RPCCommands_Server` | - | - | `onCommand` (server) | -- events: the member name is the event name, its parameters are the arguments, its return type is what `emit*` resolves with. Server listeners get the player id before the declared arguments. Names that are not identifiers work too: `'buy-item'(item: string): boolean` +- events: the member name is the event name, its parameters are the arguments, its return type is what `emit*` resolves with. Server `onClient` and `onWebview` listeners get the player id before the declared arguments. Names that are not identifiers work too: `'buy-item'(item: string): boolean` - commands: the key is the command name, the value is not used (`true`) ## License