init
This commit is contained in:
parent
822679479d
commit
ce5824db81
175
.gitignore
vendored
175
.gitignore
vendored
@ -1,2 +1,175 @@
|
||||
.idea/
|
||||
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
|
||||
|
||||
# Logs
|
||||
|
||||
logs
|
||||
_.log
|
||||
npm-debug.log_
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Caches
|
||||
|
||||
.cache
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
|
||||
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||
|
||||
# Runtime data
|
||||
|
||||
pids
|
||||
_.pid
|
||||
_.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
|
||||
.stylelintcache
|
||||
|
||||
# Microbundle cache
|
||||
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
|
||||
.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
|
||||
.temp
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
|
||||
.docusaurus
|
||||
|
||||
# Serverless directories
|
||||
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
# IntelliJ based IDEs
|
||||
.idea
|
||||
|
||||
# Finder (MacOS) folder config
|
||||
.DS_Store
|
||||
|
6
.prettierrc.yaml
Normal file
6
.prettierrc.yaml
Normal file
@ -0,0 +1,6 @@
|
||||
tabWidth: 4
|
||||
printWidth: 80
|
||||
singleQuote: true
|
||||
semi: false
|
||||
arrowParens: avoid
|
||||
endOfLine: auto
|
489
README.md
489
README.md
@ -1,488 +1,15 @@
|
||||
* [Motivation](#motivation)
|
||||
* [Installation](#installation)
|
||||
* [Examples](#examples)
|
||||
* [Server to Client](#server-to-client)
|
||||
* [CEF to Server](#cef-to-server)
|
||||
* [Client to Server](#client-to-server)
|
||||
* [API](#api)
|
||||
* [Universal](#universal)
|
||||
* [register(name, callback)](#registername-callback)
|
||||
* [unregister(name)](#unregistername)
|
||||
* [call(name, args)](#callname-args)
|
||||
* [callServer(name, args)](#callservername-args)
|
||||
* [on(name, callback)](#onname-callback)
|
||||
* [off(name, callback)](#offname-callback)
|
||||
* [trigger(name, args)](#triggername-args)
|
||||
* [triggerServer(name, args)](#triggerservername-args)
|
||||
* [Server-side](#server-side-3)
|
||||
* [callClient(player, name, args)](#callclientplayer-name-args)
|
||||
* [callBrowsers(player, name, args)](#callbrowsersplayer-name-args)
|
||||
* [triggerClient(player, name, args)](#triggerclientplayer-name-args)
|
||||
* [triggerBrowsers(player, name, args)](#triggerbrowsersplayer-name-args)
|
||||
* [Client-side](#client-side-2)
|
||||
* [callBrowser(browser, name, args)](#callbrowserbrowser-name-args)
|
||||
* [triggerBrowser(browser, name, args)](#triggerbrowserbrowser-name-args)
|
||||
* [CEF or Client-side](#cef-or-client-side)
|
||||
* [callBrowsers(name, args)](#callbrowsersname-args)
|
||||
* [callClient(name, args)](#callclientname-args)
|
||||
* [triggerBrowsers(name, args)](#triggerbrowsersname-args)
|
||||
* [triggerClient(name, args)](#triggerclientname-args)
|
||||
* [Options](#options)
|
||||
* [Events](#events)
|
||||
* [Changelog](#changelog)
|
||||
# framework-rpc
|
||||
|
||||
## Motivation
|
||||
To install dependencies:
|
||||
|
||||
A very common workflow when developing with any kind of client-server platform is not only sending data between the server and clients, but also receiving data back after performing some kind of action. An example would be a client asking for information from a database in order to display to the user. One technique to achieve this is called [remote procedure calls (RPC)](https://en.wikipedia.org/wiki/Remote_procedure_call) which allows one application context to call code in a completely separate context and return the result back to the caller, as if it were local to begin with.
|
||||
|
||||
In RAGE Multiplayer, this kind of functionality is not supported natively. In order for a player to ask something of the server, the server must set up an event handler that the player calls remotely, then the server does its processing and calls _another_ event handler that resides on the client. There are many pitfalls to this approach, including but not limited to messy code and false identification (am i sending the response to the right caller instance?). Natively, the server cannot directly communicate with CEF instances at all. You have to route *all requests* through the client. Suddenly, you have 16 different events to handle one simple data request. It's horrible. And when your codebase starts growing, it becomes a huge hassle to deal with.
|
||||
|
||||
This is pretty much what everybody has learned to deal with, until now. `rage-rpc` simplifies two-way communication between the RAGE Multiplayer server, client, and browser instances by providing a easy-to-use API for calling remote code and retrieving results. **Any context can call a function that resides in any other context and immediately get access to its return value without messing with events.** This means any CEF instance can call code on the server, the client, or any other CEF instances and easily see the result.
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
#### Option 1
|
||||
|
||||
You can install via [npm](https://github.com/npm/cli)
|
||||
|
||||
```
|
||||
npm i -S rage-rpc
|
||||
```bash
|
||||
bun install
|
||||
```
|
||||
|
||||
From here, you can simply require the package in any RAGE context:
|
||||
To run:
|
||||
|
||||
```javascript
|
||||
const rpc = require('rage-rpc');
|
||||
|
||||
rpc.register('hi', () => 'hello!');
|
||||
```bash
|
||||
bun run src/index.ts
|
||||
```
|
||||
|
||||
#### Option 2
|
||||
|
||||
In the `dist/` folder of this repository is a single minified JS file that you can download and require into any RAGE context. It works the same as the above option, but you'll have to manually redownload the file when new versions are released.
|
||||
|
||||
```javascript
|
||||
const rpc = require('./rage-rpc.min.js');
|
||||
|
||||
rpc.register('hi', () => 'hello!');
|
||||
```
|
||||
|
||||
#### Option 3 (Browser Only)
|
||||
|
||||
In order to use `require` in the browser, you'll need either an AMD loader or some kind of bundler like Webpack. If those options don't suit your project, you can load the file into browser contexts with just a script tag before the code you use it in. It will expose a global `rpc` variable that you can use on your page.
|
||||
|
||||
```html
|
||||
<html>
|
||||
<head>
|
||||
<title>My CEF Page</title>
|
||||
<script type="text/javascript" src="./rage-rpc.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
rpc.register('hi', () => 'hello from cef!');
|
||||
|
||||
// ...
|
||||
</script>
|
||||
</head>
|
||||
</html>
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Server to Client
|
||||
|
||||
**Situation:** The server wants to ask a specific player if they are currently climbing anything.
|
||||
|
||||
##### Client-side
|
||||
```javascript
|
||||
const rpc = require('rage-rpc');
|
||||
|
||||
rpc.register('getIsClimbing', () => mp.players.local.isClimbing());
|
||||
```
|
||||
|
||||
##### Server-side
|
||||
```javascript
|
||||
const rpc = require('rage-rpc');
|
||||
|
||||
const player = mp.players.at(0);
|
||||
|
||||
rpc.callClient(player, 'getIsClimbing').then(climbing => {
|
||||
if(climbing){
|
||||
console.log('The player is climbing!');
|
||||
}else{
|
||||
console.log('The player is not climbing!');
|
||||
}
|
||||
});
|
||||
|
||||
// or even just this inside an async function:
|
||||
const isClimbing = await rpc.callClient(player, 'getIsClimbing');
|
||||
```
|
||||
|
||||
**_That's it!_** No extra code to sort out who is asking for what, or setting up multiple events on each side just to send a single piece of data back to the caller.
|
||||
|
||||
---
|
||||
|
||||
### CEF to Server
|
||||
|
||||
**Situation:** A CEF instance wants a list of all vehicle license plates directly from the server.
|
||||
|
||||
##### Server-side
|
||||
```javascript
|
||||
const rpc = require('rage-rpc');
|
||||
|
||||
rpc.register('getAllLicensePlates', () => mp.vehicles.toArray().map(vehicle => vehicle.numberPlate));
|
||||
```
|
||||
|
||||
##### Client-side
|
||||
```javascript
|
||||
// even if not using RPC on the client, it must be required somewhere before CEF can send any events
|
||||
require('rage-rpc');
|
||||
```
|
||||
|
||||
##### Browser
|
||||
```javascript
|
||||
const rpc = require('rage-rpc');
|
||||
|
||||
rpc.callServer('getAllLicensePlates').then(plates => {
|
||||
alert(plates.join(', '));
|
||||
});
|
||||
```
|
||||
|
||||
With `rage-rpc`, CEF can directly communicate with the server and vice-versa, without having to pass everything through the client-side JS.
|
||||
|
||||
###### In vanilla RAGE, you would have to set up multiple events for sending/receiving on the client-side, call them from CEF, then resend the data to the server and back. It's a huge hassle.
|
||||
|
||||
---
|
||||
|
||||
### Client to Server
|
||||
|
||||
**Situation:** Give the clients/CEF the ability to log to the server's console.
|
||||
|
||||
##### Server-side
|
||||
```javascript
|
||||
const rpc = require('rage-rpc');
|
||||
|
||||
rpc.register('log', (message, info) => {
|
||||
/*
|
||||
the second argument, info, gives information about the request such as
|
||||
- the internal ID of the request
|
||||
- the environment in which the request was sent (server, client, or cef)
|
||||
- the player who sent the request, if any
|
||||
*/
|
||||
|
||||
console.log(info.player.name+': '+message);
|
||||
});
|
||||
```
|
||||
|
||||
##### Client-side OR Browser
|
||||
```javascript
|
||||
const rpc = require('rage-rpc');
|
||||
|
||||
function log(message){
|
||||
return rpc.callServer('log', message);
|
||||
}
|
||||
|
||||
// send it and forget it
|
||||
log("Hello, Server!");
|
||||
|
||||
// send it again, but make sure it was successfully received
|
||||
log("Hello again!").then(() => {
|
||||
// the server acknowledged and processed the message
|
||||
}).catch(() => {
|
||||
// the message either timed out or the procedure was never registered
|
||||
});
|
||||
```
|
||||
|
||||
**Note:** Once any side of the game registers a procedure, any context can immediately start accessing it. You could call `rpc.callServer('log', message);` from any CEF instance or anywhere in the client without any further setup.
|
||||
|
||||
## API
|
||||
|
||||
This library is universal to RAGE, which means you can load the same package into all 3 contexts: browser, client JS, and server JS.
|
||||
|
||||
There are only 7 functions that you can use almost anywhere around your game. However, depending on the current context, the usage of some functions might differ slightly.
|
||||
|
||||
### Universal
|
||||
|
||||
#### register(name, callback)
|
||||
|
||||
Registers a procedure in the current context.
|
||||
|
||||
The return value of the `callback` will be sent back to the caller, even if it fails. If a [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) is returned, it will finish before returning its result or error to the caller.
|
||||
|
||||
**The return value must be JSON-able in order to be sent over the network.** This doesn't matter if the procedure call is local.
|
||||
|
||||
##### Parameters
|
||||
|
||||
* `name` [string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String) - The unique identifier, relative to the current context, of the procedure.
|
||||
* `callback` [function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function) - The procedure. This function will receive 2 arguments.
|
||||
* `args` - The arguments that were provided by the caller. This parameter's type will be the same that was sent by the caller. `undefined` if no arguments were sent.
|
||||
* `info` [object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object) - Various information about the caller.
|
||||
* `id` [string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String) - The internal ID used to keep track of this request.
|
||||
* `environment` [string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String) - The caller's environment. Can be `cef`, `client`, or `server`.
|
||||
* `player` [Player](https://wiki.rage.mp/index.php?title=Server-side_functions#Player) - The caller. *Only exists in the server context if remotely called from `cef` or `client`.*
|
||||
|
||||
##### Examples
|
||||
|
||||
```javascript
|
||||
rpc.register('hello', () => 'hi!');
|
||||
```
|
||||
|
||||
Returns `hi!` to the caller.
|
||||
|
||||
---
|
||||
|
||||
```javascript
|
||||
rpc.register('getUser', async (id) => {
|
||||
const user = await someLongOperationThatReturnsUserFromId(id);
|
||||
return user;
|
||||
});
|
||||
```
|
||||
|
||||
Waits for the returned [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) to finish before returning the resolved user to the caller.
|
||||
|
||||
---
|
||||
|
||||
```javascript
|
||||
rpc.register('echo', (message, info) => {
|
||||
console.log(`${info.player.name} via ${info.environment}: ${message}`);
|
||||
});
|
||||
```
|
||||
|
||||
*Server-side example only.* The passed argument will be logged to the console along with the caller's name and the environment which they called from.
|
||||
|
||||
#### unregister(name)
|
||||
|
||||
Unregisters a procedure from the current context. It will no longer take requests unless it is re-registered.
|
||||
|
||||
* `name` [string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String) - The unique identifier, relative to the current context, of the procedure.
|
||||
|
||||
#### call(name, args?, options?)
|
||||
|
||||
Calls a procedure that has been registered in the current context.
|
||||
|
||||
* `name` [string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String) - The name of the previously registered procedure.
|
||||
* `args?` - Optional arguments to pass to the procedure. Can be of any type, since `call` does not traverse the network.
|
||||
* `options?` - Optional [options](#options) to control how the procedure is called.
|
||||
|
||||
##### Example
|
||||
|
||||
```javascript
|
||||
rpc.register('hi', () => 'hello!');
|
||||
|
||||
rpc.call('hi').then(result => {
|
||||
// result = hello!
|
||||
console.log(result);
|
||||
}).catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
```
|
||||
|
||||
###### Returns [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) resolving or failing due to the procedure's result. If the procedure called does not exist, `PROCEDURE_NOT_FOUND` will be thrown.
|
||||
|
||||
#### callServer(name, args?, options?)
|
||||
|
||||
Calls a procedure that has been registered on the server.
|
||||
|
||||
* `name` [string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String) - The name of the previously registered procedure.
|
||||
* `args?` - Optional arguments to pass to the procedure. Must be JSON-able if the current context is not the server. Use an array or object to pass multiple arguments.
|
||||
* `options?` - Optional [options](#options) to control how the procedure is called.
|
||||
|
||||
##### Example
|
||||
|
||||
Server-side:
|
||||
```javascript
|
||||
rpc.register('getWeather', () => mp.world.weather);
|
||||
```
|
||||
|
||||
Client-side OR Browser OR Server:
|
||||
```javascript
|
||||
rpc.callServer('getWeather').then(weather => {
|
||||
mp.gui.chat.push(`The current weather is ${weather}.`);
|
||||
}).catch(err => {
|
||||
// handle error
|
||||
});
|
||||
```
|
||||
|
||||
###### Returns [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) resolving or failing due to the procedure's result. If the procedure called does not exist, `PROCEDURE_NOT_FOUND` will be thrown.
|
||||
|
||||
### Server-side
|
||||
|
||||
#### callClient(player, name, args?)
|
||||
|
||||
Calls a procedure that has been registered on a specific client.
|
||||
|
||||
* `player` [Player](https://wiki.rage.mp/index.php?title=Server-side_functions#Player) - The player to call the procedure on.
|
||||
* `name` [string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String) - The name of the registered procedure.
|
||||
* `args?` - Optional arguments to pass to the procedure. Must be JSON-able. Use an array or object to pass multiple arguments.
|
||||
* `options?` - Optional [options](#options) to control how the procedure is called.
|
||||
|
||||
##### Example
|
||||
|
||||
Client-side:
|
||||
```javascript
|
||||
rpc.register('toggleChat', toggle => {
|
||||
mp.gui.chat.show(toggle);
|
||||
});
|
||||
```
|
||||
|
||||
Server-side:
|
||||
```javascript
|
||||
mp.players.forEach(player => {
|
||||
rpc.callClient(player, 'toggleChat', false);
|
||||
});
|
||||
```
|
||||
|
||||
###### Returns [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) resolving or failing due to the procedure's result. If the procedure called does not exist, `PROCEDURE_NOT_FOUND` will be thrown.
|
||||
|
||||
#### callBrowsers(player, name, args?, options?)
|
||||
|
||||
Calls a procedure that has been registered in any CEF instance on a specific client.
|
||||
|
||||
Any CEF instance can register the procedure. The client will iterate through each instance and call the procedure on the first instance that it exists on.
|
||||
|
||||
* `player` [Player](https://wiki.rage.mp/index.php?title=Server-side_functions#Player) - The player to call the procedure on.
|
||||
* `name` [string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String) - The name of the registered procedure.
|
||||
* `args?` - Optional arguments to pass to the procedure. Must be JSON-able. Use an array or object to pass multiple arguments.
|
||||
* `options?` - Optional [options](#options) to control how the procedure is called.
|
||||
|
||||
##### Example
|
||||
|
||||
Browser:
|
||||
```javascript
|
||||
rpc.register('toggleHUD', toggle => {
|
||||
// if jQuery is your thing
|
||||
$('#hud').toggle(toggle);
|
||||
});
|
||||
```
|
||||
|
||||
Server-side:
|
||||
```javascript
|
||||
mp.players.forEach(player => {
|
||||
rpc.callClient(player, 'toggleChat', false);
|
||||
});
|
||||
```
|
||||
|
||||
###### Returns [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) resolving or failing due to the procedure's result. If the procedure called does not exist, `PROCEDURE_NOT_FOUND` will be thrown.
|
||||
|
||||
### Client-side
|
||||
|
||||
#### callBrowser(browser, name, args?, options?)
|
||||
|
||||
Calls a procedure that has been registered in a specific CEF instance.
|
||||
|
||||
* `browser` [Browser](https://wiki.rage.mp/index.php?title=Client-side_functions#Browser) - The browser to call the procedure on.
|
||||
* `name` [string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String) - The name of the registered procedure.
|
||||
* `args?` - Optional arguments to pass to the procedure. Must be JSON-able. Use an array or object to pass multiple arguments.
|
||||
* `options?` - Optional [options](#options) to control how the procedure is called.
|
||||
|
||||
##### Example
|
||||
|
||||
Browser:
|
||||
```javascript
|
||||
rpc.register('getInputValue', () => {
|
||||
// if jQuery is your thing
|
||||
return $('#input').val();
|
||||
});
|
||||
```
|
||||
|
||||
Client-side:
|
||||
```javascript
|
||||
const browser = mp.browsers.at(0);
|
||||
|
||||
rpc.callBrowser(browser, 'getInputValue').then(value => {
|
||||
mp.gui.chat.push(`The CEF input value is: ${value}`);
|
||||
}).catch(err => {
|
||||
// handle errors
|
||||
});
|
||||
```
|
||||
|
||||
###### Returns [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) resolving or failing due to the procedure's result. If the procedure called does not exist, `PROCEDURE_NOT_FOUND` will be thrown.
|
||||
|
||||
### CEF or Client-side
|
||||
|
||||
#### callBrowsers(name, args?, options?)
|
||||
|
||||
Calls a procedure that has been registered in any CEF instance on a specific client.
|
||||
|
||||
Any CEF instance can register the procedure. The client will iterate through each instance and call the procedure on the first instance that it exists on.
|
||||
|
||||
* `name` [string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String) - The name of the registered procedure.
|
||||
* `args?` - Optional arguments to pass to the procedure. Must be JSON-able. Use an array or object to pass multiple arguments.
|
||||
* `options?` - Optional [options](#options) to control how the procedure is called.
|
||||
|
||||
##### Example
|
||||
|
||||
Browser:
|
||||
```javascript
|
||||
rpc.register('toggleHUD', toggle => {
|
||||
// if jQuery is your thing
|
||||
$('#hud').toggle(toggle);
|
||||
});
|
||||
```
|
||||
|
||||
Client-side OR Browser:
|
||||
```javascript
|
||||
rpc.callBrowsers('toggleChat', false);
|
||||
```
|
||||
|
||||
###### Returns [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) resolving or failing due to the procedure's result. If the procedure called does not exist, `PROCEDURE_NOT_FOUND` will be thrown.
|
||||
|
||||
#### callClient(name, args?, options?)
|
||||
|
||||
Calls a procedure that has been registered on the local client.
|
||||
|
||||
* `name` [string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String) - The name of the registered procedure.
|
||||
* `args?` - Optional arguments to pass to the procedure. Must be JSON-able if the current context is not this client. Use an array or object to pass multiple arguments.
|
||||
* `options?` - Optional [options](#options) to control how the procedure is called.
|
||||
|
||||
##### Example
|
||||
|
||||
Client-side:
|
||||
```javascript
|
||||
rpc.register('toggleChat', toggle => {
|
||||
mp.gui.chat.show(toggle);
|
||||
});
|
||||
```
|
||||
|
||||
Client-side OR Browser:
|
||||
```javascript
|
||||
rpc.callClient('toggleChat', false);
|
||||
```
|
||||
|
||||
###### Returns [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) resolving or failing due to the procedure's result. If the procedure called does not exist, `PROCEDURE_NOT_FOUND` will be thrown.
|
||||
|
||||
## Options
|
||||
|
||||
For remote procedure calling functions, there are optional options you can pass as the last parameter:
|
||||
|
||||
* timeout (number): The amount of time in milliseconds to reject the call automatically
|
||||
* noRet (boolean): Prevent the remote context from sending data back. Saves bandwidth, but the promise will never return or reject. Similar to using `trigger`.
|
||||
|
||||
## Events
|
||||
|
||||
You can now use rage-rpc as a full on replacement for mp.events. API functions that start with "trigger" use the same syntax as the ones that start with "call", except they do not return anything. They call remote events on any context where there can be many handlers or none.
|
||||
|
||||
## Changelog
|
||||
|
||||
Check the releases tab for an up-to-date changelog.
|
||||
|
||||
#### 0.1.0
|
||||
|
||||
* ADD: Bundled Typescript definitions
|
||||
* IMPROVE: CEF outgoing call returning performance
|
||||
* IMRPOVE: `callBrowsers` performance on all contexts
|
||||
* FIX: Some code simplifications
|
||||
|
||||
#### 0.0.3
|
||||
|
||||
* ADD: Extra player verification for outgoing server calls
|
||||
* FIX: Bug that prevented multiple resources from using RPC at the same time
|
||||
* FIX: False alarm for multiple CEF instances receiving the same result
|
||||
|
||||
#### 0.0.2
|
||||
|
||||
* FIX: UMD exposing for correct Node.js importing
|
||||
|
||||
#### 0.0.1
|
||||
|
||||
* Initial commit
|
||||
This project was created using `bun init` in bun v1.1.25. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
|
||||
|
44
dist/rage-rpc.d.ts
vendored
44
dist/rage-rpc.d.ts
vendored
@ -1,44 +0,0 @@
|
||||
export as namespace rpc;
|
||||
|
||||
export function register(name: string, cb: ProcedureListener): Function;
|
||||
export function unregister(name: string): void;
|
||||
export function call<T = any>(name: string, args?: any, options?: CallOptions): Promise<T>;
|
||||
export function callServer<T = any>(name: string, args?: any, options?: CallOptions): Promise<T>;
|
||||
export function callClient<T = any>(player: Player, name: string, args?: any, options?: CallOptions): Promise<T>;
|
||||
export function callClient<T = any>(name: string, args?: any, options?: CallOptions): Promise<T>;
|
||||
export function callBrowsers<T = any>(player: Player, name: string, args?: any, options?: CallOptions): Promise<T>;
|
||||
export function callBrowsers<T = any>(name: string, args?: any, options?: CallOptions): Promise<T>;
|
||||
export function callBrowser<T = any>(browser: Browser, name: string, args?: any, options?: CallOptions): Promise<T>;
|
||||
|
||||
export function on(name: string, cb: ProcedureListener): Function;
|
||||
export function off(name: string, cb: ProcedureListener): void;
|
||||
export function trigger(name: string, args?: any): void;
|
||||
export function triggerServer(name: string, args?: any): void;
|
||||
export function triggerClient(player: Player, name: string, args?: any): void;
|
||||
export function triggerClient(name: string, args?: any): void;
|
||||
export function triggerBrowsers(player: Player, name: string, args?: any): void;
|
||||
export function triggerBrowsers(name: string, args?: any): void;
|
||||
export function triggerBrowser(browser: Browser, name: string, args?: any): void;
|
||||
|
||||
export interface Player {
|
||||
call: (eventName: string, args?: any[]) => void;
|
||||
[property: string]: any;
|
||||
}
|
||||
|
||||
export interface Browser {
|
||||
execute: (code: string) => void;
|
||||
[property: string]: any;
|
||||
}
|
||||
|
||||
export interface ProcedureListenerInfo {
|
||||
environment: string;
|
||||
id?: string;
|
||||
player?: Player;
|
||||
}
|
||||
|
||||
export interface CallOptions {
|
||||
timeout?: number;
|
||||
noRet?: boolean;
|
||||
}
|
||||
|
||||
export type ProcedureListener = (args: any, info: ProcedureListenerInfo) => any;
|
1
dist/rage-rpc.min.js
vendored
1
dist/rage-rpc.min.js
vendored
File diff suppressed because one or more lines are too long
9
index.d.ts
vendored
Normal file
9
index.d.ts
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
declare const mp: any
|
||||
|
||||
interface Window {
|
||||
rpcEvents: Record<string, Function>
|
||||
}
|
||||
|
||||
declare const global: {
|
||||
rpcEvents: any
|
||||
}
|
5258
package-lock.json
generated
5258
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
40
package.json
40
package.json
@ -1,36 +1,12 @@
|
||||
{
|
||||
"name": "rage-rpc",
|
||||
"version": "0.4.0",
|
||||
"description": "An asynchronous RPC implementation for RAGE Multiplayer",
|
||||
"main": "dist/rage-rpc.min.js",
|
||||
"types": "dist/rage-rpc.d.ts",
|
||||
"scripts": {
|
||||
"watch": "webpack-cli --config ./webpack.config.js --mode=development --watch",
|
||||
"build": "webpack-cli --config ./webpack.config.js --mode=production",
|
||||
"type-check": "tsc"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/micaww/rage-rpc.git"
|
||||
},
|
||||
"author": "micaww",
|
||||
"license": "ISC",
|
||||
"bugs": {
|
||||
"url": "https://github.com/micaww/rage-rpc/issues"
|
||||
},
|
||||
"homepage": "https://github.com/micaww/rage-rpc#readme",
|
||||
"dependencies": {},
|
||||
"name": "framework-rpc",
|
||||
"module": "src/index.ts",
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.7.7",
|
||||
"@babel/preset-env": "^7.7.7",
|
||||
"@babel/preset-typescript": "^7.7.7",
|
||||
"babel-loader": "^8.0.6",
|
||||
"replace-in-file-webpack-plugin": "^1.0.6",
|
||||
"typescript": "^3.7.4",
|
||||
"webpack": "^4.41.4",
|
||||
"webpack-cli": "^3.3.10"
|
||||
"@types/bun": "latest",
|
||||
"prettier": "^3.3.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.0.0"
|
||||
}
|
||||
}
|
39
src/client.ts
Normal file
39
src/client.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { utils } from './utils.ts'
|
||||
import { state } from './index.ts'
|
||||
import type { RPCData } from './types.ts'
|
||||
|
||||
export class Client {
|
||||
public async listen(data: string) {
|
||||
const parsedData = utils.parseData(data)
|
||||
await this.transferTo(parsedData)
|
||||
}
|
||||
|
||||
private transferTo(data: RPCData) {
|
||||
switch (data.to) {
|
||||
case utils.environment.CLIENT:
|
||||
return this.executeLocal(data)
|
||||
case utils.environment.CEF:
|
||||
// transfer to cef
|
||||
}
|
||||
}
|
||||
|
||||
private async executeLocal(data: RPCData) {
|
||||
const fnResponse = await state[data.eventName](...data.data)
|
||||
|
||||
const response = {
|
||||
data: fnResponse,
|
||||
...data,
|
||||
}
|
||||
|
||||
this.sendResponseToServer(response)
|
||||
}
|
||||
|
||||
private sendResponseToServer(data: RPCData) {
|
||||
const eventName = utils.generateResponseEventName(data.uuid)
|
||||
const prepareForTransfer = utils.stringifyData(data)
|
||||
|
||||
mp.events.callRemote(eventName, prepareForTransfer)
|
||||
}
|
||||
}
|
||||
|
||||
export const client = new Client()
|
42
src/defs.d.ts
vendored
42
src/defs.d.ts
vendored
@ -1,42 +0,0 @@
|
||||
declare var mp: any;
|
||||
declare var global: any;
|
||||
declare var window: any;
|
||||
|
||||
declare type ProcedureListener = (args: any, info: ProcedureListenerInfo) => any;
|
||||
|
||||
declare interface Player {
|
||||
call: (eventName: string, args?: any[]) => void;
|
||||
[property: string]: any;
|
||||
}
|
||||
|
||||
declare interface Browser {
|
||||
url: string;
|
||||
execute: (code: string) => void;
|
||||
[property: string]: any;
|
||||
}
|
||||
|
||||
declare interface ProcedureListenerInfo {
|
||||
environment: string;
|
||||
id?: string;
|
||||
player?: Player;
|
||||
browser?: Browser;
|
||||
}
|
||||
|
||||
declare interface CallOptions {
|
||||
timeout?: number;
|
||||
noRet?: boolean;
|
||||
}
|
||||
|
||||
declare interface Event {
|
||||
req?: number;
|
||||
ret?: number;
|
||||
b?: string;
|
||||
id: string;
|
||||
name?: string;
|
||||
args?: any;
|
||||
env: string;
|
||||
fenv?: string;
|
||||
res?: any;
|
||||
err?: any;
|
||||
noRet?: number;
|
||||
}
|
3
src/events.ts
Normal file
3
src/events.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export const RPC_LISTENER = 'rpc::listener'
|
||||
|
||||
export const CLIENT_ROUTER_LISTENER = 'rpc::clientRouterListener'
|
600
src/index.ts
600
src/index.ts
@ -1,568 +1,58 @@
|
||||
import * as util from './util';
|
||||
import { RPC_LISTENER } from './events.ts'
|
||||
import { utils } from './utils.ts'
|
||||
|
||||
const environment = util.getEnvironment();
|
||||
if(!environment) throw 'Unknown RAGE environment';
|
||||
import { client } from './client.ts'
|
||||
import type { RPCData } from './types.ts'
|
||||
|
||||
const ERR_NOT_FOUND = 'PROCEDURE_NOT_FOUND';
|
||||
const environment = utils.getEnvironment()
|
||||
|
||||
const IDENTIFIER = '__rpc:id';
|
||||
const PROCESS_EVENT = '__rpc:process';
|
||||
const BROWSER_REGISTER = '__rpc:browserRegister';
|
||||
const BROWSER_UNREGISTER = '__rpc:browserUnregister';
|
||||
const TRIGGER_EVENT = '__rpc:triggerEvent';
|
||||
const TRIGGER_EVENT_BROWSERS = '__rpc:triggerEventBrowsers';
|
||||
export const state =
|
||||
environment === utils.environment.CEF ? window.rpcEvents : global.rpcEvents
|
||||
|
||||
const glob = environment === 'cef' ? window : global;
|
||||
|
||||
if(!glob[PROCESS_EVENT]){
|
||||
glob.__rpcListeners = {};
|
||||
glob.__rpcPending = {};
|
||||
glob.__rpcEvListeners = {};
|
||||
|
||||
glob[PROCESS_EVENT] = (player: Player | string, rawData?: string) => {
|
||||
if(environment !== "server") rawData = player as string;
|
||||
const data: Event = util.parseData(rawData);
|
||||
|
||||
if(data.req){ // someone is trying to remotely call a procedure
|
||||
const info: ProcedureListenerInfo = {
|
||||
id: data.id,
|
||||
environment: data.fenv || data.env
|
||||
};
|
||||
if(environment === "server") info.player = player as Player;
|
||||
const part = {
|
||||
ret: 1,
|
||||
id: data.id,
|
||||
env: environment
|
||||
};
|
||||
let ret: (ev: Event) => void;
|
||||
class frameworkRpc {
|
||||
constructor() {
|
||||
mp.events.add(RPC_LISTENER, async (player: any, data: string) => {
|
||||
switch (environment) {
|
||||
case "server":
|
||||
ret = ev => info.player.call(PROCESS_EVENT, [util.stringifyData(ev)]);
|
||||
break;
|
||||
case "client": {
|
||||
if(data.env === "server"){
|
||||
ret = ev => mp.events.callRemote(PROCESS_EVENT, util.stringifyData(ev));
|
||||
}else if(data.env === "cef"){
|
||||
const browser = data.b && glob.__rpcBrowsers[data.b];
|
||||
info.browser = browser;
|
||||
ret = ev => browser && util.isBrowserValid(browser) && passEventToBrowser(browser, ev, true);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "cef": {
|
||||
ret = ev => mp.trigger(PROCESS_EVENT, util.stringifyData(ev));
|
||||
}
|
||||
}
|
||||
if(ret){
|
||||
const promise = callProcedure(data.name, data.args, info);
|
||||
if(!data.noRet) promise.then(res => ret({ ...part, res })).catch(err => ret({ ...part, err: err ? err : null }));
|
||||
}
|
||||
}else if(data.ret){ // a previously called remote procedure has returned
|
||||
const info = glob.__rpcPending[data.id];
|
||||
if(environment === "server" && info.player !== player) return;
|
||||
if(info){
|
||||
info.resolve(data.hasOwnProperty('err') ? util.promiseReject(data.err) : util.promiseResolve(data.res));
|
||||
delete glob.__rpcPending[data.id];
|
||||
}
|
||||
}
|
||||
};
|
||||
case utils.environment.UNKNOWN:
|
||||
return
|
||||
|
||||
if(environment !== "cef"){
|
||||
mp.events.add(PROCESS_EVENT, glob[PROCESS_EVENT]);
|
||||
case utils.environment.CLIENT:
|
||||
player = data
|
||||
return client.listen(player)
|
||||
|
||||
if(environment === "client"){
|
||||
// set up internal pass-through events
|
||||
register('__rpc:callServer', ([name, args, noRet], info) => _callServer(name, args, { fenv: info.environment, noRet }));
|
||||
register('__rpc:callBrowsers', ([name, args, noRet], info) => _callBrowsers(null, name, args, { fenv: info.environment, noRet }));
|
||||
|
||||
// set up browser identifiers
|
||||
glob.__rpcBrowsers = {};
|
||||
const initBrowser = (browser: Browser): void => {
|
||||
const id = util.uid();
|
||||
Object.keys(glob.__rpcBrowsers).forEach(key => {
|
||||
const b = glob.__rpcBrowsers[key];
|
||||
if(!b || !util.isBrowserValid(b) || b === browser) delete glob.__rpcBrowsers[key];
|
||||
});
|
||||
glob.__rpcBrowsers[id] = browser;
|
||||
browser.execute(`
|
||||
window.name = '${id}';
|
||||
if(typeof window['${IDENTIFIER}'] === 'undefined'){
|
||||
window['${IDENTIFIER}'] = Promise.resolve(window.name);
|
||||
}else{
|
||||
window['${IDENTIFIER}:resolve'](window.name);
|
||||
case utils.environment.SERVER:
|
||||
case utils.environment.CEF:
|
||||
}
|
||||
`);
|
||||
};
|
||||
mp.browsers.forEach(initBrowser);
|
||||
mp.events.add('browserCreated', initBrowser);
|
||||
|
||||
// set up browser registration map
|
||||
glob.__rpcBrowserProcedures = {};
|
||||
mp.events.add(BROWSER_REGISTER, (data: string) => {
|
||||
const [browserId, name] = JSON.parse(data);
|
||||
glob.__rpcBrowserProcedures[name] = browserId;
|
||||
});
|
||||
mp.events.add(BROWSER_UNREGISTER, (data: string) => {
|
||||
const [browserId, name] = JSON.parse(data);
|
||||
if(glob.__rpcBrowserProcedures[name] === browserId) delete glob.__rpcBrowserProcedures[name];
|
||||
});
|
||||
|
||||
register(TRIGGER_EVENT_BROWSERS, ([name, args], info) => {
|
||||
Object.values(glob.__rpcBrowsers).forEach(browser => {
|
||||
_callBrowser(browser, TRIGGER_EVENT, [name, args], { fenv: info.environment, noRet: 1 });
|
||||
});
|
||||
});
|
||||
})
|
||||
}
|
||||
}else{
|
||||
if(typeof glob[IDENTIFIER] === 'undefined'){
|
||||
glob[IDENTIFIER] = new Promise(resolve => {
|
||||
if (window.name) {
|
||||
resolve(window.name);
|
||||
}else{
|
||||
glob[IDENTIFIER+':resolve'] = resolve;
|
||||
|
||||
public register<Args extends any[] = unknown[], Return = unknown>(
|
||||
eventName: string,
|
||||
cb: (...args: Args) => Return,
|
||||
) {
|
||||
if (environment === utils.environment.UNKNOWN) return
|
||||
state[eventName] = cb
|
||||
}
|
||||
});
|
||||
|
||||
public callClient<Args extends any[] = unknown[], Return = unknown>(
|
||||
player: any,
|
||||
eventName: string,
|
||||
...args: Args
|
||||
): Promise<Return | unknown> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const uuid = utils.generateUUID()
|
||||
|
||||
const data: RPCData = {
|
||||
uuid,
|
||||
eventName,
|
||||
from: environment,
|
||||
to: utils.environment.CLIENT,
|
||||
data: args,
|
||||
}
|
||||
|
||||
player.call(RPC_LISTENER, [utils.stringifyData(data)])
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
register(TRIGGER_EVENT, ([name, args], info) => callEvent(name, args, info));
|
||||
}
|
||||
|
||||
function passEventToBrowser(browser: Browser, data: Event, ignoreNotFound: boolean): void {
|
||||
const raw = util.stringifyData(data);
|
||||
browser.execute(`var process = window["${PROCESS_EVENT}"]; if(process){ process(${JSON.stringify(raw)}); }else{ ${ignoreNotFound ? '' : `mp.trigger("${PROCESS_EVENT}", '{"ret":1,"id":"${data.id}","err":"${ERR_NOT_FOUND}","env":"cef"}');`} }`);
|
||||
}
|
||||
|
||||
function callProcedure(name: string, args: any, info: ProcedureListenerInfo): Promise<any> {
|
||||
const listener = glob.__rpcListeners[name];
|
||||
if(!listener) return util.promiseReject(ERR_NOT_FOUND);
|
||||
return util.promiseResolve(listener(args, info));
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a procedure.
|
||||
* @param {string} name - The name of the procedure.
|
||||
* @param {function} cb - The procedure's callback. The return value will be sent back to the caller.
|
||||
* @returns {Function} The function, which unregister the event.
|
||||
*/
|
||||
export function register(name: string, cb: ProcedureListener): Function {
|
||||
if(arguments.length !== 2) throw 'register expects 2 arguments: "name" and "cb"';
|
||||
if(environment === "cef") glob[IDENTIFIER].then((id: string) => mp.trigger(BROWSER_REGISTER, JSON.stringify([id, name])));
|
||||
glob.__rpcListeners[name] = cb;
|
||||
|
||||
return () => unregister(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister a procedure.
|
||||
* @param {string} name - The name of the procedure.
|
||||
*/
|
||||
export function unregister(name: string): void {
|
||||
if(arguments.length !== 1) throw 'unregister expects 1 argument: "name"';
|
||||
if(environment === "cef") glob[IDENTIFIER].then((id: string) => mp.trigger(BROWSER_UNREGISTER, JSON.stringify([id, name])));
|
||||
glob.__rpcListeners[name] = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls a local procedure. Only procedures registered in the same context will be resolved.
|
||||
*
|
||||
* Can be called from any environment.
|
||||
*
|
||||
* @param name - The name of the locally registered procedure.
|
||||
* @param args - Any parameters for the procedure.
|
||||
* @param options - Any options.
|
||||
* @returns The result from the procedure.
|
||||
*/
|
||||
export function call(name: string, args?: any, options: CallOptions = {}): Promise<any> {
|
||||
if(arguments.length < 1 || arguments.length > 3) return util.promiseReject('call expects 1 to 3 arguments: "name", optional "args", and optional "options"');
|
||||
return util.promiseTimeout(callProcedure(name, args, { environment }), options.timeout);
|
||||
}
|
||||
|
||||
function _callServer(name: string, args?: any, extraData: any = {}): Promise<any> {
|
||||
switch(environment){
|
||||
case "server": {
|
||||
return call(name, args);
|
||||
}
|
||||
case "client": {
|
||||
const id = util.uid();
|
||||
return new Promise(resolve => {
|
||||
if(!extraData.noRet){
|
||||
glob.__rpcPending[id] = {
|
||||
resolve
|
||||
};
|
||||
}
|
||||
const event: Event = {
|
||||
req: 1,
|
||||
id,
|
||||
name,
|
||||
env: environment,
|
||||
args,
|
||||
...extraData
|
||||
};
|
||||
mp.events.callRemote(PROCESS_EVENT, util.stringifyData(event));
|
||||
});
|
||||
}
|
||||
case "cef": {
|
||||
return callClient('__rpc:callServer', [name, args, +extraData.noRet]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls a remote procedure registered on the server.
|
||||
*
|
||||
* Can be called from any environment.
|
||||
*
|
||||
* @param name - The name of the registered procedure.
|
||||
* @param args - Any parameters for the procedure.
|
||||
* @param options - Any options.
|
||||
* @returns The result from the procedure.
|
||||
*/
|
||||
export function callServer(name: string, args?: any, options: CallOptions = {}): Promise<any> {
|
||||
if(arguments.length < 1 || arguments.length > 3) return util.promiseReject('callServer expects 1 to 3 arguments: "name", optional "args", and optional "options"');
|
||||
|
||||
let extraData: any = {};
|
||||
if(options.noRet) extraData.noRet = 1;
|
||||
|
||||
return util.promiseTimeout(_callServer(name, args, extraData), options.timeout);
|
||||
}
|
||||
|
||||
function _callClient(player: Player, name: string, args?: any, extraData: any = {}): Promise<any> {
|
||||
switch(environment){
|
||||
case 'client': {
|
||||
return call(name, args);
|
||||
}
|
||||
case 'server': {
|
||||
const id = util.uid();
|
||||
return new Promise(resolve => {
|
||||
if(!extraData.noRet){
|
||||
glob.__rpcPending[id] = {
|
||||
resolve,
|
||||
player
|
||||
};
|
||||
}
|
||||
const event: Event = {
|
||||
req: 1,
|
||||
id,
|
||||
name,
|
||||
env: environment,
|
||||
args,
|
||||
...extraData
|
||||
};
|
||||
player.call(PROCESS_EVENT, [util.stringifyData(event)]);
|
||||
});
|
||||
}
|
||||
case 'cef': {
|
||||
const id = util.uid();
|
||||
return glob[IDENTIFIER].then((browserId: string) => {
|
||||
return new Promise(resolve => {
|
||||
if(!extraData.noRet){
|
||||
glob.__rpcPending[id] = {
|
||||
resolve
|
||||
};
|
||||
}
|
||||
const event: Event = {
|
||||
b: browserId,
|
||||
req: 1,
|
||||
id,
|
||||
name,
|
||||
env: environment,
|
||||
args,
|
||||
...extraData
|
||||
};
|
||||
mp.trigger(PROCESS_EVENT, util.stringifyData(event));
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls a remote procedure registered on the client.
|
||||
*
|
||||
* Can be called from any environment.
|
||||
*
|
||||
* @param player - The player to call the procedure on.
|
||||
* @param name - The name of the registered procedure.
|
||||
* @param args - Any parameters for the procedure.
|
||||
* @param options - Any options.
|
||||
* @returns The result from the procedure.
|
||||
*/
|
||||
export function callClient(player: Player | string, name?: string | any, args?: any, options: CallOptions = {}): Promise<any> {
|
||||
switch(environment){
|
||||
case 'client': {
|
||||
options = args || {};
|
||||
args = name;
|
||||
name = player;
|
||||
player = null;
|
||||
if((arguments.length < 1 || arguments.length > 3) || typeof name !== 'string') return util.promiseReject('callClient from the client expects 1 to 3 arguments: "name", optional "args", and optional "options"');
|
||||
break;
|
||||
}
|
||||
case 'server': {
|
||||
if((arguments.length < 2 || arguments.length > 4) || typeof player !== 'object') return util.promiseReject('callClient from the server expects 2 to 4 arguments: "player", "name", optional "args", and optional "options"');
|
||||
break;
|
||||
}
|
||||
case 'cef': {
|
||||
options = args || {};
|
||||
args = name;
|
||||
name = player;
|
||||
player = null;
|
||||
if((arguments.length < 1 || arguments.length > 3) || typeof name !== 'string') return util.promiseReject('callClient from the browser expects 1 to 3 arguments: "name", optional "args", and optional "options"');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let extraData: any = {};
|
||||
if(options.noRet) extraData.noRet = 1;
|
||||
|
||||
return util.promiseTimeout(_callClient(player as Player, name, args, extraData), options.timeout);
|
||||
}
|
||||
|
||||
function _callBrowser(browser: Browser, name: string, args?: any, extraData: any = {}): Promise<any> {
|
||||
return new Promise(resolve => {
|
||||
const id = util.uid();
|
||||
if(!extraData.noRet){
|
||||
glob.__rpcPending[id] = {
|
||||
resolve
|
||||
};
|
||||
}
|
||||
passEventToBrowser(browser, {
|
||||
req: 1,
|
||||
id,
|
||||
name,
|
||||
env: environment,
|
||||
args,
|
||||
...extraData
|
||||
}, false);
|
||||
});
|
||||
}
|
||||
|
||||
function _callBrowsers(player: Player, name: string, args?: any, extraData: any = {}): Promise<any> {
|
||||
switch(environment){
|
||||
case 'client':
|
||||
const browserId = glob.__rpcBrowserProcedures[name];
|
||||
if(!browserId) return util.promiseReject(ERR_NOT_FOUND);
|
||||
const browser = glob.__rpcBrowsers[browserId];
|
||||
if(!browser || !util.isBrowserValid(browser)) return util.promiseReject(ERR_NOT_FOUND);
|
||||
return _callBrowser(browser, name, args, extraData);
|
||||
case 'server':
|
||||
return _callClient(player, '__rpc:callBrowsers', [name, args, +extraData.noRet], extraData);
|
||||
case 'cef':
|
||||
return _callClient(null, '__rpc:callBrowsers', [name, args, +extraData.noRet], extraData);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls a remote procedure registered in any browser context.
|
||||
*
|
||||
* Can be called from any environment.
|
||||
*
|
||||
* @param player - The player to call the procedure on.
|
||||
* @param name - The name of the registered procedure.
|
||||
* @param args - Any parameters for the procedure.
|
||||
* @param options - Any options.
|
||||
* @returns The result from the procedure.
|
||||
*/
|
||||
export function callBrowsers(player: Player | string, name?: string | any, args?: any, options: CallOptions = {}): Promise<any> {
|
||||
let promise;
|
||||
let extraData: any = {};
|
||||
|
||||
switch(environment){
|
||||
case 'client':
|
||||
case 'cef':
|
||||
options = args || {};
|
||||
args = name;
|
||||
name = player;
|
||||
if(arguments.length < 1 || arguments.length > 3) return util.promiseReject('callBrowsers from the client or browser expects 1 to 3 arguments: "name", optional "args", and optional "options"');
|
||||
if(options.noRet) extraData.noRet = 1;
|
||||
promise = _callBrowsers(null, name, args, extraData);
|
||||
break;
|
||||
case 'server':
|
||||
if(arguments.length < 2 || arguments.length > 4) return util.promiseReject('callBrowsers from the server expects 2 to 4 arguments: "player", "name", optional "args", and optional "options"');
|
||||
if(options.noRet) extraData.noRet = 1;
|
||||
promise = _callBrowsers(player as Player, name, args, extraData);
|
||||
break;
|
||||
}
|
||||
|
||||
if(promise){
|
||||
return util.promiseTimeout(promise, options.timeout);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls a remote procedure registered in a specific browser instance.
|
||||
*
|
||||
* Client-side environment only.
|
||||
*
|
||||
* @param browser - The browser instance.
|
||||
* @param name - The name of the registered procedure.
|
||||
* @param args - Any parameters for the procedure.
|
||||
* @param options - Any options.
|
||||
* @returns The result from the procedure.
|
||||
*/
|
||||
export function callBrowser(browser: Browser, name: string, args?: any, options: CallOptions = {}): Promise<any> {
|
||||
if(environment !== 'client') return util.promiseReject('callBrowser can only be used in the client environment');
|
||||
if(arguments.length < 2 || arguments.length > 4) return util.promiseReject('callBrowser expects 2 to 4 arguments: "browser", "name", optional "args", and optional "options"');
|
||||
|
||||
let extraData: any = {};
|
||||
if(options.noRet) extraData.noRet = 1;
|
||||
|
||||
return util.promiseTimeout(_callBrowser(browser, name, args, extraData), options.timeout);
|
||||
}
|
||||
|
||||
function callEvent(name: string, args: any, info: ProcedureListenerInfo){
|
||||
const listeners = glob.__rpcEvListeners[name];
|
||||
if(listeners){
|
||||
listeners.forEach(listener => listener(args, info));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register an event handler.
|
||||
* @param {string} name - The name of the event.
|
||||
* @param cb - The callback for the event.
|
||||
* @returns {Function} The function, which off the event.
|
||||
*/
|
||||
export function on(name: string, cb: ProcedureListener): Function {
|
||||
if(arguments.length !== 2) throw 'on expects 2 arguments: "name" and "cb"';
|
||||
|
||||
const listeners = glob.__rpcEvListeners[name] || new Set();
|
||||
listeners.add(cb);
|
||||
glob.__rpcEvListeners[name] = listeners;
|
||||
|
||||
return () => off(name, cb);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister an event handler.
|
||||
* @param {string} name - The name of the event.
|
||||
* @param cb - The callback for the event.
|
||||
*/
|
||||
export function off(name: string, cb: ProcedureListener){
|
||||
if(arguments.length !== 2) throw 'off expects 2 arguments: "name" and "cb"';
|
||||
|
||||
const listeners = glob.__rpcEvListeners[name];
|
||||
if(listeners){
|
||||
listeners.delete(cb);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers a local event. Only events registered in the same context will be triggered.
|
||||
*
|
||||
* Can be called from any environment.
|
||||
*
|
||||
* @param name - The name of the locally registered event.
|
||||
* @param args - Any parameters for the event.
|
||||
*/
|
||||
export function trigger(name: string, args?: any){
|
||||
if(arguments.length < 1 || arguments.length > 2) throw 'trigger expects 1 or 2 arguments: "name", and optional "args"';
|
||||
callEvent(name, args, { environment });
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers an event registered on the client.
|
||||
*
|
||||
* Can be called from any environment.
|
||||
*
|
||||
* @param player - The player to call the procedure on.
|
||||
* @param name - The name of the event.
|
||||
* @param args - Any parameters for the event.
|
||||
*/
|
||||
export function triggerClient(player: Player | string, name?: string | any, args?: any){
|
||||
switch(environment){
|
||||
case 'client': {
|
||||
args = name;
|
||||
name = player;
|
||||
player = null;
|
||||
if((arguments.length < 1 || arguments.length > 2) || typeof name !== 'string') throw 'triggerClient from the client expects 1 or 2 arguments: "name", and optional "args"';
|
||||
break;
|
||||
}
|
||||
case 'server': {
|
||||
if((arguments.length < 2 || arguments.length > 3) || typeof player !== 'object') throw 'triggerClient from the server expects 2 or 3 arguments: "player", "name", and optional "args"';
|
||||
break;
|
||||
}
|
||||
case 'cef': {
|
||||
args = name;
|
||||
name = player;
|
||||
player = null;
|
||||
if((arguments.length < 1 || arguments.length > 2) || typeof name !== 'string') throw 'triggerClient from the browser expects 1 or 2 arguments: "name", and optional "args"';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_callClient(player as Player, TRIGGER_EVENT, [name, args], { noRet: 1 });
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers an event registered on the server.
|
||||
*
|
||||
* Can be called from any environment.
|
||||
*
|
||||
* @param name - The name of the event.
|
||||
* @param args - Any parameters for the event.
|
||||
*/
|
||||
export function triggerServer(name: string, args?: any){
|
||||
if(arguments.length < 1 || arguments.length > 2) throw 'triggerServer expects 1 or 2 arguments: "name", and optional "args"';
|
||||
|
||||
_callServer(TRIGGER_EVENT, [name, args], { noRet: 1 });
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers an event registered in any browser context.
|
||||
*
|
||||
* Can be called from any environment.
|
||||
*
|
||||
* @param player - The player to call the procedure on.
|
||||
* @param name - The name of the event.
|
||||
* @param args - Any parameters for the event.
|
||||
*/
|
||||
export function triggerBrowsers(player: Player | string, name?: string | any, args?: any){
|
||||
switch(environment){
|
||||
case 'client':
|
||||
case 'cef':
|
||||
args = name;
|
||||
name = player;
|
||||
player = null;
|
||||
if(arguments.length < 1 || arguments.length > 2) throw 'triggerBrowsers from the client or browser expects 1 or 2 arguments: "name", and optional "args"';
|
||||
break;
|
||||
case 'server':
|
||||
if(arguments.length < 2 || arguments.length > 3) throw 'triggerBrowsers from the server expects 2 or 3 arguments: "player", "name", and optional "args"';
|
||||
break;
|
||||
}
|
||||
|
||||
_callClient(player as Player, TRIGGER_EVENT_BROWSERS, [name, args], { noRet: 1 });
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers an event registered in a specific browser instance.
|
||||
*
|
||||
* Client-side environment only.
|
||||
*
|
||||
* @param browser - The browser instance.
|
||||
* @param name - The name of the event.
|
||||
* @param args - Any parameters for the event.
|
||||
*/
|
||||
export function triggerBrowser(browser: Browser, name: string, args?: any){
|
||||
if(environment !== 'client') throw 'callBrowser can only be used in the client environment';
|
||||
if(arguments.length < 2 || arguments.length > 4) throw 'callBrowser expects 2 or 3 arguments: "browser", "name", and optional "args"';
|
||||
|
||||
_callBrowser(browser, TRIGGER_EVENT, [name, args], { noRet: 1});
|
||||
}
|
||||
|
||||
export default {
|
||||
register,
|
||||
unregister,
|
||||
call,
|
||||
callServer,
|
||||
callClient,
|
||||
callBrowsers,
|
||||
callBrowser,
|
||||
on,
|
||||
off,
|
||||
trigger,
|
||||
triggerServer,
|
||||
triggerClient,
|
||||
triggerBrowsers,
|
||||
triggerBrowser
|
||||
};
|
||||
const test = new frameworkRpc()
|
||||
|
9
src/types.ts
Normal file
9
src/types.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { Environment } from './utils.ts'
|
||||
|
||||
export type RPCData = {
|
||||
data?: any
|
||||
from: Environment
|
||||
to: Environment
|
||||
eventName: string
|
||||
uuid: string
|
||||
}
|
122
src/util.ts
122
src/util.ts
@ -1,122 +0,0 @@
|
||||
enum MpTypes {
|
||||
Blip = 'b',
|
||||
Checkpoint = 'cp',
|
||||
Colshape = 'c',
|
||||
Label = 'l',
|
||||
Marker = 'm',
|
||||
Object = 'o',
|
||||
Pickup = 'p',
|
||||
Player = 'pl',
|
||||
Vehicle = 'v'
|
||||
}
|
||||
|
||||
function isObjectMpType(obj: any, type: MpTypes){
|
||||
const client = getEnvironment() === 'client';
|
||||
if(obj && typeof obj === 'object' && typeof obj.id !== 'undefined'){
|
||||
const test = (type, collection, mpType) => client ? obj.type === type && collection.at(obj.id) === obj : obj instanceof mpType;
|
||||
switch(type){
|
||||
case MpTypes.Blip: return test('blip', mp.blips, mp.Blip);
|
||||
case MpTypes.Checkpoint: return test('checkpoint', mp.checkpoints, mp.Checkpoint);
|
||||
case MpTypes.Colshape: return test('colshape', mp.colshapes, mp.Colshape);
|
||||
case MpTypes.Label: return test('textlabel', mp.labels, mp.TextLabel);
|
||||
case MpTypes.Marker: return test('marker', mp.markers, mp.Marker);
|
||||
case MpTypes.Object: return test('object', mp.objects, mp.Object);
|
||||
case MpTypes.Pickup: return test('pickup', mp.pickups, mp.Pickup);
|
||||
case MpTypes.Player: return test('player', mp.players, mp.Player);
|
||||
case MpTypes.Vehicle: return test('vehicle', mp.vehicles, mp.Vehicle);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function uid(): string {
|
||||
const first = (Math.random() * 46656) | 0;
|
||||
const second = (Math.random() * 46656) | 0;
|
||||
const firstPart = ('000' + first.toString(36)).slice(-3);
|
||||
const secondPart = ('000' + second.toString(36)).slice(-3);
|
||||
return firstPart + secondPart;
|
||||
}
|
||||
|
||||
export function getEnvironment(): string {
|
||||
if(mp.joaat) return 'server';
|
||||
else if(mp.game && mp.game.joaat) return 'client';
|
||||
else if(mp.trigger) return 'cef';
|
||||
}
|
||||
|
||||
export function stringifyData(data: any): string {
|
||||
const env = getEnvironment();
|
||||
return JSON.stringify(data, (_, value) => {
|
||||
if(env === 'client' || env === 'server' && value && typeof value === 'object'){
|
||||
let type;
|
||||
|
||||
if(isObjectMpType(value, MpTypes.Blip)) type = MpTypes.Blip;
|
||||
else if(isObjectMpType(value, MpTypes.Checkpoint)) type = MpTypes.Checkpoint;
|
||||
else if(isObjectMpType(value, MpTypes.Colshape)) type = MpTypes.Colshape;
|
||||
else if(isObjectMpType(value, MpTypes.Marker)) type = MpTypes.Marker;
|
||||
else if(isObjectMpType(value, MpTypes.Object)) type = MpTypes.Object;
|
||||
else if(isObjectMpType(value, MpTypes.Pickup)) type = MpTypes.Pickup;
|
||||
else if(isObjectMpType(value, MpTypes.Player)) type = MpTypes.Player;
|
||||
else if(isObjectMpType(value, MpTypes.Vehicle)) type = MpTypes.Vehicle;
|
||||
|
||||
if(type) return {
|
||||
__t: type,
|
||||
i: typeof value.remoteId === 'number' ? value.remoteId : value.id
|
||||
};
|
||||
}
|
||||
|
||||
return value;
|
||||
});
|
||||
}
|
||||
|
||||
export function parseData(data: string): any {
|
||||
const env = getEnvironment();
|
||||
return JSON.parse(data, (_, value) => {
|
||||
if((env === 'client' || env === 'server') && value && typeof value === 'object' && typeof value['__t'] === 'string' && typeof value.i === 'number' && Object.keys(value).length === 2){
|
||||
const id = value.i;
|
||||
const type = value['__t'];
|
||||
let collection;
|
||||
|
||||
switch(type){
|
||||
case MpTypes.Blip: collection = mp.blips; break;
|
||||
case MpTypes.Checkpoint: collection = mp.checkpoints; break;
|
||||
case MpTypes.Colshape: collection = mp.colshapes; break;
|
||||
case MpTypes.Label: collection = mp.labels; break;
|
||||
case MpTypes.Marker: collection = mp.markers; break;
|
||||
case MpTypes.Object: collection = mp.objects; break;
|
||||
case MpTypes.Pickup: collection = mp.pickups; break;
|
||||
case MpTypes.Player: collection = mp.players; break;
|
||||
case MpTypes.Vehicle: collection = mp.vehicles; break;
|
||||
}
|
||||
|
||||
if(collection) return collection[env === 'client' ? 'atRemoteId' : 'at'](id);
|
||||
}
|
||||
|
||||
return value;
|
||||
});
|
||||
}
|
||||
|
||||
export function promiseResolve(result: any): Promise<any> {
|
||||
return new Promise(resolve => setTimeout(() => resolve(result), 0));
|
||||
}
|
||||
|
||||
export function promiseReject(error: any): Promise<any> {
|
||||
return new Promise((_, reject) => setTimeout(() => reject(error), 0));
|
||||
}
|
||||
|
||||
export function promiseTimeout(promise: Promise<any>, timeout?: number){
|
||||
if(typeof timeout === 'number'){
|
||||
return Promise.race([
|
||||
new Promise((_, reject) => {
|
||||
setTimeout(() => reject('TIMEOUT'), timeout);
|
||||
}),
|
||||
promise
|
||||
]);
|
||||
}else return promise;
|
||||
}
|
||||
|
||||
export function isBrowserValid(browser: Browser): boolean {
|
||||
try {
|
||||
browser.url;
|
||||
}catch(e){ return false; }
|
||||
return true;
|
||||
}
|
42
src/utils.ts
Normal file
42
src/utils.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import type { RPCData } from './types.ts'
|
||||
import { RPC_LISTENER } from './events.ts'
|
||||
|
||||
export enum Environment {
|
||||
CEF = 'CEF',
|
||||
CLIENT = 'CLIENT',
|
||||
SERVER = 'SERVER',
|
||||
UNKNOWN = 'UNKNOWN',
|
||||
}
|
||||
|
||||
function 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
|
||||
}
|
||||
|
||||
export const utils = {
|
||||
environment: Environment,
|
||||
getEnvironment: () => {
|
||||
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
|
||||
},
|
||||
parseData: (data: string): RPCData => JSON.parse(data),
|
||||
stringifyData: (data: RPCData): string => JSON.stringify(data),
|
||||
generateResponseEventName: (uuid: string) => `${RPC_LISTENER}_${uuid}`,
|
||||
generateUUID,
|
||||
}
|
@ -1,14 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["es2015"],
|
||||
// Enable latest features
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleDetection": "force",
|
||||
"jsx": "react-jsx",
|
||||
"allowJs": true,
|
||||
|
||||
// Bundler mode
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
"isolatedModules": true,
|
||||
"noImplicitAny": true
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
|
||||
// Best practices
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
|
||||
// Some stricter flags (disabled by default)
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noPropertyAccessFromIndexSignature": false
|
||||
}
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
const path = require('path');
|
||||
const ReplaceInFileWebpackPlugin = require('replace-in-file-webpack-plugin');
|
||||
|
||||
const LIBRARY_NAME = 'rpc';
|
||||
const OUTPUT_FILE = 'rage-rpc.min.js';
|
||||
|
||||
module.exports = mode => ({
|
||||
entry: './src/index.ts',
|
||||
mode,
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.ts$/,
|
||||
loader: 'babel-loader'
|
||||
}
|
||||
]
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.ts']
|
||||
},
|
||||
output: {
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
filename: OUTPUT_FILE,
|
||||
library: LIBRARY_NAME,
|
||||
libraryTarget: 'umd',
|
||||
globalObject: "typeof self !== 'undefined' ? self : this"
|
||||
},
|
||||
plugins: [
|
||||
new ReplaceInFileWebpackPlugin([{
|
||||
dir: 'dist',
|
||||
files: [OUTPUT_FILE],
|
||||
rules: [{
|
||||
search: `exports.${LIBRARY_NAME}`,
|
||||
replace: 'exports'
|
||||
}, {
|
||||
search: `exports["${LIBRARY_NAME}"]`,
|
||||
replace: 'exports'
|
||||
}]
|
||||
}])
|
||||
]
|
||||
});
|
Reference in New Issue
Block a user