mirror of
https://github.com/rilaxik/fivem-rpc.git
synced 2026-09-28 01:29:13 +00:00
Compare commits
40
Commits
821cd589d9
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0751dbd730 | ||
|
|
d1d4d91a5e | ||
|
|
9f949d3950 | ||
|
|
28b1f96b50 | ||
|
|
9a9667c480 | ||
|
|
cca07468d5 | ||
|
|
40c322e4b7 | ||
|
|
c4c9d02fd4 | ||
|
|
674eb25000 | ||
|
|
269269771d | ||
|
|
d189651689 | ||
|
|
ed66e61de0 | ||
|
|
143bee3e4e | ||
|
|
79fe874c90 | ||
|
|
50b96eeb80 | ||
|
|
8029f24e9d | ||
|
|
3e3548a9cc | ||
|
|
c085fd2c8f | ||
|
|
6247b7d781 | ||
|
|
b48d3f5240 | ||
|
|
558f04567a | ||
|
|
571d113386 | ||
|
|
79faa3b23c | ||
|
|
46933c1656 | ||
|
|
2f59e4c253 | ||
|
|
75cbdb512c | ||
|
|
b39a1d6e1b | ||
|
|
e88af4aa01 | ||
|
|
b2d086e18e | ||
|
|
e6f9f07f4f | ||
|
|
3b695eb561 | ||
|
|
14c95dd3b4 | ||
|
|
f5d390050a | ||
|
|
67ea24ab33 | ||
|
|
3f7311ba17 | ||
|
|
00b8edc16b | ||
|
|
91a72c46e8 | ||
|
|
8a361f67ec | ||
|
|
563ef864b7 | ||
|
|
43b7d16f9a |
@@ -0,0 +1,13 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = crlf
|
||||||
|
indent_style = tab
|
||||||
|
indent_size = 2
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
indent_style = space
|
||||||
|
trim_trailing_whitespace = false
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
* text=auto eol=crlf
|
||||||
|
bun.lock text eol=lf
|
||||||
@@ -2,4 +2,3 @@
|
|||||||
.vscode
|
.vscode
|
||||||
dist
|
dist
|
||||||
node_modules
|
node_modules
|
||||||
bun.lock
|
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"$schema": "./node_modules/oxfmt/configuration_schema.json",
|
||||||
|
"useTabs": true,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"printWidth": 80,
|
||||||
|
"endOfLine": "crlf",
|
||||||
|
"singleQuote": true,
|
||||||
|
"semi": false,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"arrowParens": "avoid",
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"sortImports": true,
|
||||||
|
"ignorePatterns": ["**/dist", "bun.lock"],
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": ["*.md"],
|
||||||
|
"options": { "embeddedLanguageFormatting": "off" }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||||
|
"plugins": ["eslint", "typescript", "unicorn", "oxc", "import", "jsdoc"],
|
||||||
|
"categories": {
|
||||||
|
"correctness": "error",
|
||||||
|
"suspicious": "warn"
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"typescript/no-explicit-any": "error",
|
||||||
|
"typescript/no-unsafe-function-type": "error",
|
||||||
|
"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"]
|
||||||
|
}
|
||||||
-40
@@ -1,40 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
|
|
||||||
"vcs": {
|
|
||||||
"enabled": false,
|
|
||||||
"clientKind": "git",
|
|
||||||
"useIgnoreFile": false
|
|
||||||
},
|
|
||||||
"files": {
|
|
||||||
"ignoreUnknown": false
|
|
||||||
},
|
|
||||||
"formatter": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"linter": {
|
|
||||||
"enabled": true,
|
|
||||||
"rules": {
|
|
||||||
"recommended": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"javascript": {
|
|
||||||
"formatter": {
|
|
||||||
"arrowParentheses": "asNeeded",
|
|
||||||
"bracketSpacing": true,
|
|
||||||
"indentWidth": 2,
|
|
||||||
"lineEnding": "crlf",
|
|
||||||
"lineWidth": 80,
|
|
||||||
"quoteStyle": "single",
|
|
||||||
"semicolons": "asNeeded",
|
|
||||||
"trailingCommas": "all"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"assist": {
|
|
||||||
"enabled": true,
|
|
||||||
"actions": {
|
|
||||||
"source": {
|
|
||||||
"organizeImports": "on"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,302 @@
|
|||||||
|
{
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"configVersion": 1,
|
||||||
|
"workspaces": {
|
||||||
|
"": {
|
||||||
|
"devDependencies": {
|
||||||
|
"oxfmt": "^0.70.0",
|
||||||
|
"oxlint": "^1.85.0",
|
||||||
|
"typescript": "^7.0.2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"rpc": {
|
||||||
|
"name": "@entityseven/fivem-rpc",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@entityseven/fivem-rpc-shared-types": "workspace:^",
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"tsdown": "^0.23.0",
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": ">=5",
|
||||||
|
},
|
||||||
|
"optionalPeers": [
|
||||||
|
"typescript",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"shared-types": {
|
||||||
|
"name": "@entityseven/fivem-rpc-shared-types",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": ">=5",
|
||||||
|
},
|
||||||
|
"optionalPeers": [
|
||||||
|
"typescript",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"packages": {
|
||||||
|
"@entityseven/fivem-rpc": ["@entityseven/fivem-rpc@workspace:rpc"],
|
||||||
|
|
||||||
|
"@entityseven/fivem-rpc-shared-types": ["@entityseven/fivem-rpc-shared-types@workspace:shared-types"],
|
||||||
|
|
||||||
|
"@oxc-project/types": ["@oxc-project/[email protected]", "", {}, "sha512-J1yXrIlNDZVzE3ada310xeAw7nH8yCAyLPuUIsjKatFPmfn5bS1oW+cM+QsGOtVWd5nhSpbwZWx/rue+r5Z+PA=="],
|
||||||
|
|
||||||
|
"@oxfmt/binding-android-arm-eabi": ["@oxfmt/[email protected]", "", { "os": "android", "cpu": "arm" }, "sha512-Xd7YO4/T2axEj6FTLcj4Why3mTBqFMg+x24xtorT4Lb2+1g82090GH0a/4U1m0pABGYiix2bq1pqkYrmV3f0Sw=="],
|
||||||
|
|
||||||
|
"@oxfmt/binding-android-arm64": ["@oxfmt/[email protected]", "", { "os": "android", "cpu": "arm64" }, "sha512-x9rlMYyKXdgKdYyUJzGsK1ZV8P4di/J32ipzcS6Jet6p9r9UAh28neXIMtdlSaJJycdi61Z4YkcLKLpk8ueFjg=="],
|
||||||
|
|
||||||
|
"@oxfmt/binding-darwin-arm64": ["@oxfmt/[email protected]", "", { "os": "darwin", "cpu": "arm64" }, "sha512-IUTUPvrBVYy7POh4stXzRdz4IVC/1QSaviCWoyenSlOhGu0X9j5K07vCTM9biLjAA2Zs31l0Rj5vvRpj9n95wA=="],
|
||||||
|
|
||||||
|
"@oxfmt/binding-darwin-x64": ["@oxfmt/[email protected]", "", { "os": "darwin", "cpu": "x64" }, "sha512-vw745q870oTd6J517O24asoX4/E+eK0nxYIFoedSLqgJ+nI5En7+ZS82iZSHZ69zevQrnOXiyHP01dA+t8xD8w=="],
|
||||||
|
|
||||||
|
"@oxfmt/binding-freebsd-x64": ["@oxfmt/[email protected]", "", { "os": "freebsd", "cpu": "x64" }, "sha512-NO14EgSM9dFkcg+MfGPxvsKqXYs9LKaxPrOKXpv1R0rLokGGFDcCq6dBMq18dE4wlpFOovX0UZY2uh1P30O7QA=="],
|
||||||
|
|
||||||
|
"@oxfmt/binding-linux-arm-gnueabihf": ["@oxfmt/[email protected]", "", { "os": "linux", "cpu": "arm" }, "sha512-139OEhHarj9CYoJ/i9gXlPv4KLBGtLj2toseOWYFf09QwlhklaZk+wW3aOvlqoeZtuayvkoSNVWra7WJ21s3VQ=="],
|
||||||
|
|
||||||
|
"@oxfmt/binding-linux-arm-musleabihf": ["@oxfmt/[email protected]", "", { "os": "linux", "cpu": "arm" }, "sha512-GEh2PY3IWTE0M24eNhTduountANSbWyDmMnzFSQE/nGg/bjPugbUgiGuFu+xdqcQSd/HKSwH80/F2yVVD48yhA=="],
|
||||||
|
|
||||||
|
"@oxfmt/binding-linux-arm64-gnu": ["@oxfmt/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-En5i+UJmZSPxuSf47F2Hl5YOzKB0bicQLnGQkeTCMQ35cWLtbrSwACJKfiLqRZrk05DwSnsJkhBRaM3OURtIaA=="],
|
||||||
|
|
||||||
|
"@oxfmt/binding-linux-arm64-musl": ["@oxfmt/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-WWOoV5W9Im3flVwOVrWn/2DUlOF8v5vcCip+kcNuaMpulRCh6nzzt1Su2vcL2F908YJIXNV3HvegbBHuyLwKHg=="],
|
||||||
|
|
||||||
|
"@oxfmt/binding-linux-ppc64-gnu": ["@oxfmt/[email protected]", "", { "os": "linux", "cpu": "ppc64" }, "sha512-YUouneIqW+5n7aE8xx/zeZ6/utr/KH7oykcGoFyd8Uz8uh591T1oKlnoWA3BsRq/ZR42oY1w4MUYvS/0e/MQOA=="],
|
||||||
|
|
||||||
|
"@oxfmt/binding-linux-riscv64-gnu": ["@oxfmt/[email protected]", "", { "os": "linux", "cpu": "none" }, "sha512-iEnMf21S5aGVa4hViDGY8sAQ/AHyCu2JPyrQF8P06wtHhSkD1YJBeT4m/KiGewgf7+a5XCYSCRIPcRQa1xwEoQ=="],
|
||||||
|
|
||||||
|
"@oxfmt/binding-linux-riscv64-musl": ["@oxfmt/[email protected]", "", { "os": "linux", "cpu": "none" }, "sha512-91Sdniaj20fQzyMeCxMDzTP4c9s4RB8dGQ308xHhDR0n6U7+1Xq7N9klE7mfXq8iV3lRmIGSXi5X23Hn/0XX/g=="],
|
||||||
|
|
||||||
|
"@oxfmt/binding-linux-s390x-gnu": ["@oxfmt/[email protected]", "", { "os": "linux", "cpu": "s390x" }, "sha512-uUV30M6E+2TKKGMaKiwfeL4RZrviHXlUxsrYJ/jFBb+1EZy+pnFT+hF73eeWdzh5NqPOAnw0iiMAIqjqiLZPFg=="],
|
||||||
|
|
||||||
|
"@oxfmt/binding-linux-x64-gnu": ["@oxfmt/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-ivMcX6kNDPhqtbOaBt/ItFlLlTlXNHLgRuNmxP6Na6UuYXRT10llpJcAPbGeRgjjb3Qzv4jwPp3fB0hui50WNQ=="],
|
||||||
|
|
||||||
|
"@oxfmt/binding-linux-x64-musl": ["@oxfmt/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-w+S+fERxYmlZSyZlJK/U292FjyBoH8cCEj21/tYJX6atX5kNSn+HDkhlFQKT2zcMwUW0uAtUL/bOrlwJZwqRdA=="],
|
||||||
|
|
||||||
|
"@oxfmt/binding-openharmony-arm64": ["@oxfmt/[email protected]", "", { "os": "none", "cpu": "arm64" }, "sha512-Zlom1Xkx257R8bk4ZI4zJsrGno2opknz1+5v5baka3nn4FPyvNSdh8JUL4CdN1S1OWRMvJ9UJQ+RfIqGGCUEfA=="],
|
||||||
|
|
||||||
|
"@oxfmt/binding-win32-arm64-msvc": ["@oxfmt/[email protected]", "", { "os": "win32", "cpu": "arm64" }, "sha512-FQgPW5R17vzt7cgrJ8eG/dqX00o2xHsqFeLfw4xzA9FRHpN/DjFo9YDonvIIXGxiEuS9F/jZPnGO+KHNKuCo4Q=="],
|
||||||
|
|
||||||
|
"@oxfmt/binding-win32-ia32-msvc": ["@oxfmt/[email protected]", "", { "os": "win32", "cpu": "ia32" }, "sha512-ZfZublNhZ+XBndMiXhkiLlPE+XyGRDa0CweeTL6t1fZypfCh1LTg7e5CvnOeTBunq15MskOcRempumSPGAaCQA=="],
|
||||||
|
|
||||||
|
"@oxfmt/binding-win32-x64-msvc": ["@oxfmt/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-HlIZEn+WzLQL0DszNzldiRl/DPRCX5R0Vkt6qeUPR1YHwy52hZZo4x6HoTOVmKRP2wUiwPGtKsihNY/f8KRaBg=="],
|
||||||
|
|
||||||
|
"@oxlint/binding-android-arm-eabi": ["@oxlint/[email protected]", "", { "os": "android", "cpu": "arm" }, "sha512-q2KO/Zso9UT+OMn0NF9ywn4E4t0MI3yxiDhNyhsQ7DyQJrC4FhFE4TXOi4bktFnOWXTMds8qZSbpv2XwRaNOBg=="],
|
||||||
|
|
||||||
|
"@oxlint/binding-android-arm64": ["@oxlint/[email protected]", "", { "os": "android", "cpu": "arm64" }, "sha512-SxLN3ALjoT9NNdvpjEevGeHvfzTAFrF0NBYB5tzK7/GtCKMze3j1e/m/X2ozqGj2U9hfGG/dg/OG8vpVK4PiDA=="],
|
||||||
|
|
||||||
|
"@oxlint/binding-darwin-arm64": ["@oxlint/[email protected]", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Y/Sup/J4f0f9UGsSd/xyCNTeWL+gepO63GBdEDAfue9nBsnk9zMmnIXx1O6b1V8C90vB5nucYNZ0pbMXAp8zJA=="],
|
||||||
|
|
||||||
|
"@oxlint/binding-darwin-x64": ["@oxlint/[email protected]", "", { "os": "darwin", "cpu": "x64" }, "sha512-ApOSNC04ynpDTwvBD+//0wyfODRSbEzvRoKpX8teffmc27z8AockwSNeMXGJXn5KP85eahDgR/2llICWLkzcnw=="],
|
||||||
|
|
||||||
|
"@oxlint/binding-freebsd-x64": ["@oxlint/[email protected]", "", { "os": "freebsd", "cpu": "x64" }, "sha512-bNrVrCOA/kHky3Tu79IXWXe5bhIgLXfUuUEDHlAGOHUk96MkvDZ1ecaQF19rwstrnaqfP1o9nBTqzIr9+ZHkUg=="],
|
||||||
|
|
||||||
|
"@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/[email protected]", "", { "os": "linux", "cpu": "arm" }, "sha512-NUrzOJ1s/EqsVvfn2L/1D8Wro2LPIZUbihL8kOJLh5fEdGEN3rdOGUYq3HwnUIL8sjpoP+4N6RaGrgmMJnaMPw=="],
|
||||||
|
|
||||||
|
"@oxlint/binding-linux-arm-musleabihf": ["@oxlint/[email protected]", "", { "os": "linux", "cpu": "arm" }, "sha512-UJXrAT3E/RWkEqXLIs2ehETja1qfgkPb+5gwLIIS+o/6cf+grHvoOXTa5997a/YNQfcJS0DRBTOfZt95cvOI1g=="],
|
||||||
|
|
||||||
|
"@oxlint/binding-linux-arm64-gnu": ["@oxlint/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-lK40QLjI0HxigO7CjDDshEtfYIeiYS0020v5BHFPqN4uuQBQxd2K9LNom2dW15o9F1937quSCRVp4ZsVhdbYdg=="],
|
||||||
|
|
||||||
|
"@oxlint/binding-linux-arm64-musl": ["@oxlint/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-c2zbdBwGKreHXwRx3gWBuFGJxLhxgsg6YlZ+3H+RgRusU/UEV9jNwJ3HGYK+nRo0LvBa7mt6Kj86xoVotUo8cw=="],
|
||||||
|
|
||||||
|
"@oxlint/binding-linux-ppc64-gnu": ["@oxlint/[email protected]", "", { "os": "linux", "cpu": "ppc64" }, "sha512-tlt/Hy8lZ97/lCPmCgw/B3k/mwh+BzaIPbPkldZEly7TwLmx0xe2CQcaW2g/rR0dOgS9JNGCZsMEqLhUNMGvaw=="],
|
||||||
|
|
||||||
|
"@oxlint/binding-linux-riscv64-gnu": ["@oxlint/[email protected]", "", { "os": "linux", "cpu": "none" }, "sha512-3tNR9Xey82X0zKuY1d8hJ6Rc9gwRDurmqGLnQZa5xqOXy8/YyiqFXjAtugkKLY82obOlpK1eSiDRlgcNPuxtIg=="],
|
||||||
|
|
||||||
|
"@oxlint/binding-linux-riscv64-musl": ["@oxlint/[email protected]", "", { "os": "linux", "cpu": "none" }, "sha512-wbGRd5PqCcjkJFHhZuZ2OBSUQY9czlQsoA/cQQB9JK/L9mC5MQgGoKAh+xd8QjA5V+0D3j+Qd1lAWn1I8zlelA=="],
|
||||||
|
|
||||||
|
"@oxlint/binding-linux-s390x-gnu": ["@oxlint/[email protected]", "", { "os": "linux", "cpu": "s390x" }, "sha512-3Sn0kSrE4DPZCWV/8o+n4x3aFZxI9ulMnkYlwCbJ8eUVkwRK2IerohE/A/z3SNbCwoPFOCJmGE5Avrq0rrvdvQ=="],
|
||||||
|
|
||||||
|
"@oxlint/binding-linux-x64-gnu": ["@oxlint/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-JY2pxxYfB62bAGfejljVCqc44etItehPuAyaeSAdMuEMtwNA00ggMnS66lC1oIhos6oOXUkuU6mZ9bpFh3BqWg=="],
|
||||||
|
|
||||||
|
"@oxlint/binding-linux-x64-musl": ["@oxlint/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-5k74vZ6qJBjBHEOlBk9B/iv68Yu0F1Afw/vvT2ar6OGCqEeXLaSjXz2n/IPCbhLG22UoKoYEJTzpYraRdcp6PA=="],
|
||||||
|
|
||||||
|
"@oxlint/binding-openharmony-arm64": ["@oxlint/[email protected]", "", { "os": "none", "cpu": "arm64" }, "sha512-GbAl5qt5TCkPLXTaIISZJnugrcBhra6rodcXc9jYt620UtdsTt71NlNmJmm0frxzFpd54x/G+MkitEJA8I/BoA=="],
|
||||||
|
|
||||||
|
"@oxlint/binding-win32-arm64-msvc": ["@oxlint/[email protected]", "", { "os": "win32", "cpu": "arm64" }, "sha512-kjmws5MK0et2swk4ND85D7NVQyDHw162i6whtZDLUA/lo6FQyBZDcmMRCMcVZcNrAhIaftVb00x9ChGDOjjNJA=="],
|
||||||
|
|
||||||
|
"@oxlint/binding-win32-ia32-msvc": ["@oxlint/[email protected]", "", { "os": "win32", "cpu": "ia32" }, "sha512-eSsIJx9n4yxvOqYTZyPEMyEXRmE60XH7xGAU7i0Qbsn1lf6Za3CWJ9aRd82oSFKXaxhp+sA6/yMJVRIpLpna6A=="],
|
||||||
|
|
||||||
|
"@oxlint/binding-win32-x64-msvc": ["@oxlint/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-pBebIPUpKKhWrhSMWhy8TdAZBewiXnfxmaAGxhzxM1068GagqFaTwgKlU6e+UyJ2sPR+VoHouhXuGJkQjsrDvA=="],
|
||||||
|
|
||||||
|
"@quansync/fs": ["@quansync/[email protected]", "", { "dependencies": { "quansync": "^1.0.0" } }, "sha512-qAPG/t3HqML1TlN7sY/pTbEjzFVAKsMjNNMGheyDosro+kT4iw2KCUoHcVdmliwWjorm4elZbgNQyU2eD97eDg=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-android-arm-eabi": ["@rolldown/[email protected]", "", { "os": "android", "cpu": "arm" }, "sha512-A5kXfGKvKWWZE0TtPrfsvT+q4Y5d1QG8gGUzpYjGydM+fARM9MuX90PrXYXe0XbsDVgyxxNzHo6giCj90bsFNw=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-android-arm64": ["@rolldown/[email protected]", "", { "os": "android", "cpu": "arm64" }, "sha512-z6cTycz+iJ4PVkuL4HHW4DfTfoeU/2nqYYuSOrTmH7yHK5Y0LCOnA03V4ZNxavyVaU1oOqUgIg2klN/s+USGOA=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-darwin-arm64": ["@rolldown/[email protected]", "", { "os": "darwin", "cpu": "arm64" }, "sha512-jShvqNtP6vDC6/A5JOAzbVV+DkgHqhl/ScVCJEbt+TUY6QYz7YnXcrg3sLtFBniro0f/Ld50ZwCWA6f7KYD1nQ=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-darwin-x64": ["@rolldown/[email protected]", "", { "os": "darwin", "cpu": "x64" }, "sha512-f2i2xiNWq1Z1l2++q2fuhZRdLAT3aqxD6vRNm1RAxpUoBcdqNB3C0s1Bt+K+PbEx2F5F4gQp6hqKkphCY/xF9w=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-freebsd-x64": ["@rolldown/[email protected]", "", { "os": "freebsd", "cpu": "x64" }, "sha512-4Ir5FSOKIAMr4r0kExpt1s3bMgzJU3rA45AYOHtQpls0oNeqcYBKrWMlckrYH4KCfGLfkfn1tN1dmZPMVsdXow=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/[email protected]", "", { "os": "linux", "cpu": "arm" }, "sha512-/gnRDM+39BROzAN/k1OZjDPnDMcZxB/0EUxKjONO5yVkNEvlsoMDrxGNKgZi/ttFriS2gwlDNzB65pvNbFOXIQ=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-PFaK8HwvAHbaKbBcDNQihjMKYvFnA5hiENx/l5tphTDz1E0WFp32l0A7aq7lyUwGsRw/xSrNIy/gIK4thrSCrw=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-linux-arm64-musl": ["@rolldown/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-AskzJUIKRLPxkruR1wLKewGbOw+EYfU/9lOrBFj4AFrEA8hPpKFnODWNu2WLaNs0QNkEb9QIJufmVZZIL/bJlg=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/[email protected]", "", { "os": "linux", "cpu": "ppc64" }, "sha512-qlUGAheh2yh8afH7QBgx0PrRHN85hKnNd78x8MeMhXivuevgd8vgf6/CstOzmNKY/lLTHvNTrPy98cLnAugzJw=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-linux-s390x-gnu": ["@rolldown/[email protected]", "", { "os": "linux", "cpu": "s390x" }, "sha512-secpEad+0vCbSfn8upFySkDskv+bGPk3THSDS9Y89yc4rb4kzqHp8Dmyd9BkQW4SnhNXBZCl/6CrO//hZahNJQ=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-linux-x64-gnu": ["@rolldown/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-mOVBT3dPpkWm8XBWPmU4bf+U6dYDLeMo/9ojUmis4N0L5uu10qra5vOyngZ7/PSdoE4G9KvRt4bloRxNjLas7A=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-linux-x64-musl": ["@rolldown/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-Is78i9A8Ui4SqcxUwFJ9uMmjDn58IbVTjFWYdQestFEgeuEmHMLGNriXnVJKkwG2YiZjw8cP0zCTyDMdDGtOOg=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-openharmony-arm64": ["@rolldown/[email protected]", "", { "os": "none", "cpu": "arm64" }, "sha512-dUCXneZ87INUMyQ0D+C0HrEBNUPNXHaPmU5GTjyKTJEiussw9Kaj5Ln8UztPe4epV/ffvgNBEadksdYhmW6xJA=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/[email protected]", "", { "os": "win32", "cpu": "arm64" }, "sha512-jByxb6qfd+bH1xUd0qnfFnb17i9sWBPY2tOavJ0l3tdr3OTu+Kvtm8cd/JV5nFt657b1VqGltxg9olOEfofXWw=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-win32-x64-msvc": ["@rolldown/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-/PzKqzAJ03i19oy2ItPvyvaVjOjBCNnfaJs8yvUdGBKmiESgnrJSQ2awd81QzFbbnAmu7YO9ZnJrDCb9VSJPRA=="],
|
||||||
|
|
||||||
|
"@rolldown/pluginutils": ["@rolldown/[email protected]", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="],
|
||||||
|
|
||||||
|
"@typescript/typescript-aix-ppc64": ["@typescript/[email protected]", "", { "os": "aix", "cpu": "ppc64" }, "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ=="],
|
||||||
|
|
||||||
|
"@typescript/typescript-darwin-arm64": ["@typescript/[email protected]", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA=="],
|
||||||
|
|
||||||
|
"@typescript/typescript-darwin-x64": ["@typescript/[email protected]", "", { "os": "darwin", "cpu": "x64" }, "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA=="],
|
||||||
|
|
||||||
|
"@typescript/typescript-freebsd-arm64": ["@typescript/[email protected]", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ=="],
|
||||||
|
|
||||||
|
"@typescript/typescript-freebsd-x64": ["@typescript/[email protected]", "", { "os": "freebsd", "cpu": "x64" }, "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw=="],
|
||||||
|
|
||||||
|
"@typescript/typescript-linux-arm": ["@typescript/[email protected]", "", { "os": "linux", "cpu": "arm" }, "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ=="],
|
||||||
|
|
||||||
|
"@typescript/typescript-linux-arm64": ["@typescript/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ=="],
|
||||||
|
|
||||||
|
"@typescript/typescript-linux-loong64": ["@typescript/[email protected]", "", { "os": "linux", "cpu": "none" }, "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ=="],
|
||||||
|
|
||||||
|
"@typescript/typescript-linux-mips64el": ["@typescript/[email protected]", "", { "os": "linux", "cpu": "none" }, "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA=="],
|
||||||
|
|
||||||
|
"@typescript/typescript-linux-ppc64": ["@typescript/[email protected]", "", { "os": "linux", "cpu": "ppc64" }, "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA=="],
|
||||||
|
|
||||||
|
"@typescript/typescript-linux-riscv64": ["@typescript/[email protected]", "", { "os": "linux", "cpu": "none" }, "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ=="],
|
||||||
|
|
||||||
|
"@typescript/typescript-linux-s390x": ["@typescript/[email protected]", "", { "os": "linux", "cpu": "s390x" }, "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw=="],
|
||||||
|
|
||||||
|
"@typescript/typescript-linux-x64": ["@typescript/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A=="],
|
||||||
|
|
||||||
|
"@typescript/typescript-netbsd-arm64": ["@typescript/[email protected]", "", { "os": "none", "cpu": "arm64" }, "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA=="],
|
||||||
|
|
||||||
|
"@typescript/typescript-netbsd-x64": ["@typescript/[email protected]", "", { "os": "none", "cpu": "x64" }, "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA=="],
|
||||||
|
|
||||||
|
"@typescript/typescript-openbsd-arm64": ["@typescript/[email protected]", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ=="],
|
||||||
|
|
||||||
|
"@typescript/typescript-openbsd-x64": ["@typescript/[email protected]", "", { "os": "openbsd", "cpu": "x64" }, "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg=="],
|
||||||
|
|
||||||
|
"@typescript/typescript-sunos-x64": ["@typescript/[email protected]", "", { "os": "sunos", "cpu": "x64" }, "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g=="],
|
||||||
|
|
||||||
|
"@typescript/typescript-win32-arm64": ["@typescript/[email protected]", "", { "os": "win32", "cpu": "arm64" }, "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ=="],
|
||||||
|
|
||||||
|
"@typescript/typescript-win32-x64": ["@typescript/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g=="],
|
||||||
|
|
||||||
|
"@yuku-codegen/binding-android-arm64": ["@yuku-codegen/[email protected]", "", { "os": "android", "cpu": "arm64" }, "sha512-Oc2KInVkPfUjEB4PeV6X0NvIoyYTzDe/WmqFrTwBqKs6YKnz4A7XwWQvJTb7M0rLz9a6WgpUUiiS5QKaCi4O/g=="],
|
||||||
|
|
||||||
|
"@yuku-codegen/binding-darwin-arm64": ["@yuku-codegen/[email protected]", "", { "os": "darwin", "cpu": "arm64" }, "sha512-3H7eNPIHJndUJXZ4QUGBPq1zC6RGcUZfm7bymJes+/N7wTVWUn1bxZ9JcozYHpkWQNm9f23G8YWbaHD9aYH72A=="],
|
||||||
|
|
||||||
|
"@yuku-codegen/binding-darwin-x64": ["@yuku-codegen/[email protected]", "", { "os": "darwin", "cpu": "x64" }, "sha512-AJOUpR2s3LF9AWiiub/Uyc/UoXNTWcq8hK3cVI6fUSXtwh34dG21J6hRuNlO6JVJFTo4o3ScVvZOrh/YFfUEAA=="],
|
||||||
|
|
||||||
|
"@yuku-codegen/binding-freebsd-x64": ["@yuku-codegen/[email protected]", "", { "os": "freebsd", "cpu": "x64" }, "sha512-ms7DcZu87u5CiK/wMwvpKAvAmtaqKjCQIXNweMLypG6qbuiaOMQf1jpbB5+j46xBcgCovo8TQMcv0bCQLKIL9w=="],
|
||||||
|
|
||||||
|
"@yuku-codegen/binding-linux-arm-gnu": ["@yuku-codegen/[email protected]", "", { "os": "linux", "cpu": "arm" }, "sha512-xJDpYAsKV5+eaiBhTYl05fvT6sst4PsCeuIFMRu9b0/WCSrNQPfCDtAQ5/MS6xNpsdujdZEPR+gmfWk3ZG5i3A=="],
|
||||||
|
|
||||||
|
"@yuku-codegen/binding-linux-arm-musl": ["@yuku-codegen/[email protected]", "", { "os": "linux", "cpu": "arm" }, "sha512-qfTkgd7AEx61l4K9VtXWCGTxc/fmXJ4ZheDz3i+/AAJUtg4sa0bPrc0VBxPggB/rayR8Z3+JfrBItuyanBJJ9Q=="],
|
||||||
|
|
||||||
|
"@yuku-codegen/binding-linux-arm64-gnu": ["@yuku-codegen/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-4e6Mifm/4UdjtU3D8mATIrvgP+xEiH8xtQOjd0zpd72XKWT7ug3sdyhq2utkkZFP6BvYp7SgZrI+aR4VeIOHdw=="],
|
||||||
|
|
||||||
|
"@yuku-codegen/binding-linux-arm64-musl": ["@yuku-codegen/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-RdsJrUfDYFVV3JOmWFUWwSu31fa1APn12xDeIKxgl/YWxrabwtxsDBNjF2561c7tmcbiewdCUvngqRs8AyGVLA=="],
|
||||||
|
|
||||||
|
"@yuku-codegen/binding-linux-x64-gnu": ["@yuku-codegen/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-mVzWimEPPreaPyXIUvxsHoJzvO7ckZ5j0LgQFHz04zQIRwt9A9T2hA7K5/jYjJKkMxWG/U2VjhGNwVhtyU+uqg=="],
|
||||||
|
|
||||||
|
"@yuku-codegen/binding-linux-x64-musl": ["@yuku-codegen/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-Y77fyISurmr2mvO1yeq3u+x/WJbACgPR4w+lzEVx30ViCxZzIGrZPRN1yEdZ9xUNPNsIO5thBHIdRNG+UGUG+Q=="],
|
||||||
|
|
||||||
|
"@yuku-codegen/binding-win32-arm64": ["@yuku-codegen/[email protected]", "", { "os": "win32", "cpu": "arm64" }, "sha512-1+tGLyG0u5YYy0lev4ck7/0sd8xJ9SZde0dLut7qLDPZV5WVTV8x6OxNXUne/PGuHZyO1vK+txPQzzHOyXHGSw=="],
|
||||||
|
|
||||||
|
"@yuku-codegen/binding-win32-x64": ["@yuku-codegen/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-8a2SExRohRbwC0MY4VpOF0/RSckrJ2ASZqAor5/RYSJJaeadR93n10gZzcKT6pY9ukiSOIZCRKVRD2tH73T0qA=="],
|
||||||
|
|
||||||
|
"@yuku-parser/binding-android-arm64": ["@yuku-parser/[email protected]", "", { "os": "android", "cpu": "arm64" }, "sha512-2VPBU9fRGRAQ2xPAvghnec3oou5Nrxm2Bezhf+13UMspAxQSkF/32r+ySKXSwIPA5/RivumDJDxAxc301Sgicw=="],
|
||||||
|
|
||||||
|
"@yuku-parser/binding-darwin-arm64": ["@yuku-parser/[email protected]", "", { "os": "darwin", "cpu": "arm64" }, "sha512-LD+PMZE51tCYTOss5HBkm3/AE39MvcMDBfWJx7A4yDcjfNbAQDnHZKtzSOuqrswx+TQHY0ws5xn6+fWOwtmfBA=="],
|
||||||
|
|
||||||
|
"@yuku-parser/binding-darwin-x64": ["@yuku-parser/[email protected]", "", { "os": "darwin", "cpu": "x64" }, "sha512-mmZ8cND+AoIIMRERyMinlg5ApHxP23B9jH2B5wT7T+dliPa9rubLxneB/SUjFwyUjGaFnB5G7t4YvpfbO5zbkQ=="],
|
||||||
|
|
||||||
|
"@yuku-parser/binding-freebsd-x64": ["@yuku-parser/[email protected]", "", { "os": "freebsd", "cpu": "x64" }, "sha512-gVIjaaIddbRfAhHlC8N809wQWml7mxfSVnzaLtzGXObTeFTPAg/YVnQXt1UOhPM1ah5eG/8Y0RUlNpB4GVe7eQ=="],
|
||||||
|
|
||||||
|
"@yuku-parser/binding-linux-arm-gnu": ["@yuku-parser/[email protected]", "", { "os": "linux", "cpu": "arm" }, "sha512-q/XPPQQAPdlw05aPj30ygBhekmQryGOwxVraBgApjKK8yY1kNQgqq6XCYLF1WHSee+lhxclrTO7W0/bt7dR1GA=="],
|
||||||
|
|
||||||
|
"@yuku-parser/binding-linux-arm-musl": ["@yuku-parser/[email protected]", "", { "os": "linux", "cpu": "arm" }, "sha512-A+Cb0I1hFF4wilTQpWs79k1aNnj4B2tkrHx3zsuUNF9BtVY2zPZ4yeQEM/zjXrS/qJlNMZVK9NrWJjwdpnTrfg=="],
|
||||||
|
|
||||||
|
"@yuku-parser/binding-linux-arm64-gnu": ["@yuku-parser/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-aGNSzqIqqphFwAdIFwVvKIyXD1Iy3CxrEJVIZAT87Ecyi4vCmmDD2v2l9h+Gd3/wSy3JZlOI792LJbKAjyy9rQ=="],
|
||||||
|
|
||||||
|
"@yuku-parser/binding-linux-arm64-musl": ["@yuku-parser/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-Ddl1sF0rtuCXyHGSOV6dcmdx+ETv1iD+IVFqQuOjzkJZWclxJxdBWX6ZJMRtTiGqGUTbqLEKiTXxpR/Bvqk+2A=="],
|
||||||
|
|
||||||
|
"@yuku-parser/binding-linux-x64-gnu": ["@yuku-parser/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-/nlcpR6IF5U+0m5L9wvAIXeQa2DT+BXuvqKDsTcOTlcc0B4dHNyqE5hTXsS4hAyimpy2ZK8A1zMNF5hTrjg2cg=="],
|
||||||
|
|
||||||
|
"@yuku-parser/binding-linux-x64-musl": ["@yuku-parser/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-GX80dxTQD/M/OryyuxJcFzFENzry3cDFPvCFTyWogNWQH3fSHfMrNfpwpl1YzMos1eV9NygK5GSDG7VArQlb4w=="],
|
||||||
|
|
||||||
|
"@yuku-parser/binding-win32-arm64": ["@yuku-parser/[email protected]", "", { "os": "win32", "cpu": "arm64" }, "sha512-agePQBV4VHewiGU0ACSjscZ/hJd283f9JfAF19Gf1rI5+wy5tTgx4GS36i0b3xim15AQ4RCpDRosEvhLZ4zAOw=="],
|
||||||
|
|
||||||
|
"@yuku-parser/binding-win32-x64": ["@yuku-parser/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-s8//CMpgL5+y1lvDCdyh1rwGI5+ytDJywFJRe1vnhI7n0j+caxshNucurikYLLVeQ2SYFex6aPrzgQxkabBQRA=="],
|
||||||
|
|
||||||
|
"@yuku-toolchain/types": ["@yuku-toolchain/[email protected]", "", {}, "sha512-sSeo4SSSToiS+sSD+bwn/s94EEcaLJ7tG5LCp8gFYC1G5VzxzX7fqB6m9RU1yMD8KTpu6zdU/I1NlQf8hVBJ9Q=="],
|
||||||
|
|
||||||
|
"cac": ["[email protected]", "", {}, "sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ=="],
|
||||||
|
|
||||||
|
"defu": ["[email protected]", "", {}, "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ=="],
|
||||||
|
|
||||||
|
"dts-resolver": ["[email protected]", "", { "peerDependencies": { "oxc-resolver": ">=11.0.0" }, "optionalPeers": ["oxc-resolver"] }, "sha512-1T1f+z+4tl9XD+m+0HBgWoL/nm0bOIffyWaUuUSBlFg/86IWvfx+wjNaO/ybU0AJzG9/Mi5hBUgGV6zCmWEN7Q=="],
|
||||||
|
|
||||||
|
"empathic": ["[email protected]", "", {}, "sha512-AnfC1ATldl49/cvZdLPDjBfrRNwbDO05aibiOtzQu3qtlbJtomNLhF30HEtn/7iBz50dlMECqATo3fG0LrdEgw=="],
|
||||||
|
|
||||||
|
"fdir": ["[email protected]", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||||
|
|
||||||
|
"get-tsconfig": ["[email protected]", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-X6fBC0pmImC70gvX2zm56go9hx0MyoGVdG0tUCkg/D+Xnh5TJsOZ7iDbOdI3PvmtrDxnu1YdDufpK2QJX1Meqw=="],
|
||||||
|
|
||||||
|
"hookable": ["[email protected]", "", {}, "sha512-+abwxtiEA52GCVIsQqut3S/uKTbUwYIp4Pe/vv+6py5XiXBCqMZHg6pA6Y5qhgLSEys0/cYuPbO0z1QFj5ZCmg=="],
|
||||||
|
|
||||||
|
"import-without-cache": ["[email protected]", "", {}, "sha512-vXoV9PjKHEednCUu01e98TkImxy67e3BJXbTIOmIq4Hyzw3IAwYRcuPkfeTzJzwyyts4kjuA73x+pWJLc4f86A=="],
|
||||||
|
|
||||||
|
"obug": ["[email protected]", "", {}, "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q=="],
|
||||||
|
|
||||||
|
"oxfmt": ["[email protected]", "", { "dependencies": { "tinypool": "2.1.2" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.70.0", "@oxfmt/binding-android-arm64": "0.70.0", "@oxfmt/binding-darwin-arm64": "0.70.0", "@oxfmt/binding-darwin-x64": "0.70.0", "@oxfmt/binding-freebsd-x64": "0.70.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.70.0", "@oxfmt/binding-linux-arm-musleabihf": "0.70.0", "@oxfmt/binding-linux-arm64-gnu": "0.70.0", "@oxfmt/binding-linux-arm64-musl": "0.70.0", "@oxfmt/binding-linux-ppc64-gnu": "0.70.0", "@oxfmt/binding-linux-riscv64-gnu": "0.70.0", "@oxfmt/binding-linux-riscv64-musl": "0.70.0", "@oxfmt/binding-linux-s390x-gnu": "0.70.0", "@oxfmt/binding-linux-x64-gnu": "0.70.0", "@oxfmt/binding-linux-x64-musl": "0.70.0", "@oxfmt/binding-openharmony-arm64": "0.70.0", "@oxfmt/binding-win32-arm64-msvc": "0.70.0", "@oxfmt/binding-win32-ia32-msvc": "0.70.0", "@oxfmt/binding-win32-x64-msvc": "0.70.0" }, "peerDependencies": { "svelte": "^5.0.0", "vite-plus": "*" }, "optionalPeers": ["svelte", "vite-plus"], "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-IsHxZ4y0wQLLMhnrJblBJgZsLDzfULrJnAw5j/QqsTlMa/m3AqsbToi+W71uhBGaqlqq/PbbjvHc09TJwdv3Tw=="],
|
||||||
|
|
||||||
|
"oxlint": ["[email protected]", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.85.0", "@oxlint/binding-android-arm64": "1.85.0", "@oxlint/binding-darwin-arm64": "1.85.0", "@oxlint/binding-darwin-x64": "1.85.0", "@oxlint/binding-freebsd-x64": "1.85.0", "@oxlint/binding-linux-arm-gnueabihf": "1.85.0", "@oxlint/binding-linux-arm-musleabihf": "1.85.0", "@oxlint/binding-linux-arm64-gnu": "1.85.0", "@oxlint/binding-linux-arm64-musl": "1.85.0", "@oxlint/binding-linux-ppc64-gnu": "1.85.0", "@oxlint/binding-linux-riscv64-gnu": "1.85.0", "@oxlint/binding-linux-riscv64-musl": "1.85.0", "@oxlint/binding-linux-s390x-gnu": "1.85.0", "@oxlint/binding-linux-x64-gnu": "1.85.0", "@oxlint/binding-linux-x64-musl": "1.85.0", "@oxlint/binding-openharmony-arm64": "1.85.0", "@oxlint/binding-win32-arm64-msvc": "1.85.0", "@oxlint/binding-win32-ia32-msvc": "1.85.0", "@oxlint/binding-win32-x64-msvc": "1.85.0" }, "peerDependencies": { "oxlint-tsgolint": ">=7.0.2001", "vite-plus": "*" }, "optionalPeers": ["oxlint-tsgolint", "vite-plus"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-bc26s97nuvPj1ViyPsqmKecVkUWFMEdtayO8MaQ6oiLfs1pj94cQlZZhrh4BPNlr9HQosjhIlwgZKsfcwmcNgg=="],
|
||||||
|
|
||||||
|
"picomatch": ["[email protected]", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="],
|
||||||
|
|
||||||
|
"quansync": ["[email protected]", "", {}, "sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA=="],
|
||||||
|
|
||||||
|
"resolve-pkg-maps": ["[email protected]", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
|
||||||
|
|
||||||
|
"rolldown": ["[email protected]", "", { "dependencies": { "@oxc-project/types": "=0.151.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm-eabi": "1.2.11", "@rolldown/binding-android-arm64": "1.2.11", "@rolldown/binding-darwin-arm64": "1.2.11", "@rolldown/binding-darwin-x64": "1.2.11", "@rolldown/binding-freebsd-x64": "1.2.11", "@rolldown/binding-linux-arm-gnueabihf": "1.2.11", "@rolldown/binding-linux-arm64-gnu": "1.2.11", "@rolldown/binding-linux-arm64-musl": "1.2.11", "@rolldown/binding-linux-ppc64-gnu": "1.2.11", "@rolldown/binding-linux-s390x-gnu": "1.2.11", "@rolldown/binding-linux-x64-gnu": "1.2.11", "@rolldown/binding-linux-x64-musl": "1.2.11", "@rolldown/binding-openharmony-arm64": "1.2.11", "@rolldown/binding-win32-arm64-msvc": "1.2.11", "@rolldown/binding-win32-x64-msvc": "1.2.11" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-qpSwIyz0jHQq5qXBTNxFmE6664rJ7O+4TvPFOiOaBSrz8IOHc1koKKSqTM2H6u1UG1+TveuC6vaDHKXFOvb1Kw=="],
|
||||||
|
|
||||||
|
"rolldown-plugin-dts": ["[email protected]", "", { "dependencies": { "dts-resolver": "^3.0.0", "get-tsconfig": "5.0.0-beta.6", "obug": "^3.0.0", "yuku-ast": "^0.10.1", "yuku-codegen": "^0.10.1", "yuku-parser": "^0.10.1" }, "peerDependencies": { "@typescript/native-preview": "*", "@volar/typescript": "~2.4.0", "@vue/language-core": "~3.2.0 || ~3.3.0", "rolldown": "^1.2.0", "typescript": "^5.0.0 || ^6.0.0 || ~7.0.0", "vue-tsc": "~3.2.0 || ~3.3.0" }, "optionalPeers": ["@typescript/native-preview", "@volar/typescript", "@vue/language-core", "typescript", "vue-tsc"] }, "sha512-qKrFtBfRfR2hP233m7Ic9zf3wv6MSYdZghWKMdEO6dRYZGlNfINJAa1P5ZVvyHh3mrcd7IJQvIY8L9vnm+v5rQ=="],
|
||||||
|
|
||||||
|
"tinyexec": ["[email protected]", "", {}, "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA=="],
|
||||||
|
|
||||||
|
"tinyglobby": ["[email protected]", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
|
||||||
|
|
||||||
|
"tinypool": ["[email protected]", "", {}, "sha512-9YodfrxS9g9IbFr/KOjE5bAeJ0p61n3bW6mqvy0jtoeKd1kTW1Cxm0oulm6KX2lyM9Gl6WIe8nEbY7LWv5ZJww=="],
|
||||||
|
|
||||||
|
"tree-kill": ["[email protected]", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="],
|
||||||
|
|
||||||
|
"tsdown": ["[email protected]", "", { "dependencies": { "cac": "^7.0.0", "defu": "^6.1.7", "empathic": "^2.0.1", "hookable": "^6.1.1", "import-without-cache": "^0.4.0", "obug": "^2.1.4", "picomatch": "^4.0.7", "rolldown": "~1.2.7", "rolldown-plugin-dts": "^0.28.5", "tinyexec": "^1.3.1", "tinyglobby": "^0.2.17", "tree-kill": "^1.2.2", "unconfig-core": "^7.5.0", "verkit": "^0.4.0" }, "peerDependencies": { "@arethetypeswrong/core": "^0.18.1", "@tsdown/css": "0.23.0", "@tsdown/exe": "0.23.0", "@vitejs/devtools": "*", "publint": "^0.3.8", "tsx": "*", "typescript": "^5.0.0 || ^6.0.0 || ^7.0.0", "unplugin-unused": ">=0.5.0", "unrun": "*" }, "optionalPeers": ["@arethetypeswrong/core", "@tsdown/css", "@tsdown/exe", "@vitejs/devtools", "publint", "tsx", "typescript", "unplugin-unused", "unrun"], "bin": { "tsdown": "./dist/run.mjs" } }, "sha512-BaT+ep1xnj5hdyICLd5r5SYYE+TXnI1ATePLykv9vcKpHpFTWUWRH+x1AF/E2qcu3YB6+vI8IdyxIIIffEqw5Q=="],
|
||||||
|
|
||||||
|
"typescript": ["[email protected]", "", { "optionalDependencies": { "@typescript/typescript-aix-ppc64": "7.0.2", "@typescript/typescript-darwin-arm64": "7.0.2", "@typescript/typescript-darwin-x64": "7.0.2", "@typescript/typescript-freebsd-arm64": "7.0.2", "@typescript/typescript-freebsd-x64": "7.0.2", "@typescript/typescript-linux-arm": "7.0.2", "@typescript/typescript-linux-arm64": "7.0.2", "@typescript/typescript-linux-loong64": "7.0.2", "@typescript/typescript-linux-mips64el": "7.0.2", "@typescript/typescript-linux-ppc64": "7.0.2", "@typescript/typescript-linux-riscv64": "7.0.2", "@typescript/typescript-linux-s390x": "7.0.2", "@typescript/typescript-linux-x64": "7.0.2", "@typescript/typescript-netbsd-arm64": "7.0.2", "@typescript/typescript-netbsd-x64": "7.0.2", "@typescript/typescript-openbsd-arm64": "7.0.2", "@typescript/typescript-openbsd-x64": "7.0.2", "@typescript/typescript-sunos-x64": "7.0.2", "@typescript/typescript-win32-arm64": "7.0.2", "@typescript/typescript-win32-x64": "7.0.2" }, "bin": { "tsc": "bin/tsc" } }, "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA=="],
|
||||||
|
|
||||||
|
"unconfig-core": ["[email protected]", "", { "dependencies": { "@quansync/fs": "^1.0.0", "quansync": "^1.0.0" } }, "sha512-Su3FauozOGP44ZmKdHy2oE6LPjk51M/TRRjHv2HNCWiDvfvCoxC2lno6jevMA91MYAdCdwP05QnWdWpSbncX/w=="],
|
||||||
|
|
||||||
|
"verkit": ["[email protected]", "", {}, "sha512-mMVzj0TXExtVdlDEq+Mzp0eyOuyznNpFobNM3uAqe3HVm/Ps2c+u17BligMkZjZCA6EiXpoon8iRTer3iu40SQ=="],
|
||||||
|
|
||||||
|
"yuku-ast": ["[email protected]", "", { "dependencies": { "@yuku-toolchain/types": "^0.10.2" } }, "sha512-UnG9mA6giglCvSErft2/40TVFX750Sj1xgwddPLpG7J7rlr/P1wPADg9G2RK+d/1tNLtRVoLdMMOMVBB9561TQ=="],
|
||||||
|
|
||||||
|
"yuku-codegen": ["[email protected]", "", { "dependencies": { "@yuku-toolchain/types": "^0.10.2" }, "optionalDependencies": { "@yuku-codegen/binding-android-arm64": "0.10.2", "@yuku-codegen/binding-darwin-arm64": "0.10.2", "@yuku-codegen/binding-darwin-x64": "0.10.2", "@yuku-codegen/binding-freebsd-x64": "0.10.2", "@yuku-codegen/binding-linux-arm-gnu": "0.10.2", "@yuku-codegen/binding-linux-arm-musl": "0.10.2", "@yuku-codegen/binding-linux-arm64-gnu": "0.10.2", "@yuku-codegen/binding-linux-arm64-musl": "0.10.2", "@yuku-codegen/binding-linux-x64-gnu": "0.10.2", "@yuku-codegen/binding-linux-x64-musl": "0.10.2", "@yuku-codegen/binding-win32-arm64": "0.10.2", "@yuku-codegen/binding-win32-x64": "0.10.2" } }, "sha512-hentl2dtrF6cPjiAirtkfxfFZBA6Hdm61UqRJ7cA0qrlDNMk9PZGOjw5w1mx3E0hu/6pHcJF4dJW+D0SXHPZOA=="],
|
||||||
|
|
||||||
|
"yuku-parser": ["[email protected]", "", { "dependencies": { "@yuku-toolchain/types": "^0.10.2", "yuku-ast": "^0.10.2" }, "optionalDependencies": { "@yuku-parser/binding-android-arm64": "0.10.2", "@yuku-parser/binding-darwin-arm64": "0.10.2", "@yuku-parser/binding-darwin-x64": "0.10.2", "@yuku-parser/binding-freebsd-x64": "0.10.2", "@yuku-parser/binding-linux-arm-gnu": "0.10.2", "@yuku-parser/binding-linux-arm-musl": "0.10.2", "@yuku-parser/binding-linux-arm64-gnu": "0.10.2", "@yuku-parser/binding-linux-arm64-musl": "0.10.2", "@yuku-parser/binding-linux-x64-gnu": "0.10.2", "@yuku-parser/binding-linux-x64-musl": "0.10.2", "@yuku-parser/binding-win32-arm64": "0.10.2", "@yuku-parser/binding-win32-x64": "0.10.2" } }, "sha512-CgaU0/PPjCAIEZ3WQroosOxTY3eeKldAN3h+vk8pMNz4+jl1CZzBR4pW+K/rRPF112VooCL5FdjJoiMNjDrL2A=="],
|
||||||
|
|
||||||
|
"rolldown-plugin-dts/obug": ["[email protected]", "", {}, "sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw=="],
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# Migrating from 0.1 to 1.0
|
||||||
|
|
||||||
|
Upgrade server, client and webview together. The payload format changed, so 0.1 and 1.0 cannot talk to each other
|
||||||
|
|
||||||
|
## Creating the instance
|
||||||
|
|
||||||
|
`RPCFactory` is gone, use `createRPC`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// 0.1
|
||||||
|
import { RPCFactory } from '@entityseven/fivem-rpc'
|
||||||
|
export const rpc = new RPCFactory({ env: 'server' }).get()
|
||||||
|
|
||||||
|
// 1.0
|
||||||
|
import { createRPC } from '@entityseven/fivem-rpc'
|
||||||
|
export const rpc = createRPC({ env: 'server' })
|
||||||
|
```
|
||||||
|
|
||||||
|
## Errors and timeouts
|
||||||
|
|
||||||
|
- a failed call now rejects on the caller with `RPCError` (`code`, `message`, `details`). In 0.1 the error was thrown on the receiving side and the call never settled
|
||||||
|
- every call times out after 5000 ms by default. Change it with `RPCConfig.timeout`, `0` restores the 0.1 behaviour (wait forever)
|
||||||
|
- `RPCErrors.INVALID_DATA` and `RPCErrors.NO_PLAYER` are removed (invalid payloads are dropped), `RPCErrors.TIMEOUT` and `RPCErrors.HANDLER_ERROR` are new
|
||||||
|
- the texts of `RPCErrors.UNKNOWN_NATIVE` and `RPCErrors.UNKNOWN_ENVIRONMENT` changed. Compare `error.code` with `RPCErrors`, not message strings
|
||||||
|
|
||||||
|
See [Errors](rpc/readme.md#errors) and [How it works](rpc/readme.md#how-it-works)
|
||||||
|
|
||||||
|
## Typing
|
||||||
|
|
||||||
|
Declarations are now module augmentation of interfaces, the `types` / `typeRoots` tsconfig setup is no longer needed. Follow the [shared-types readme](shared-types/readme.md), then:
|
||||||
|
|
||||||
|
- commands are interface keys instead of string unions:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// 0.1
|
||||||
|
export type RPCCommands_Server = 'ban' | 'kick'
|
||||||
|
|
||||||
|
// 1.0
|
||||||
|
interface RPCCommands_Server {
|
||||||
|
ban: true
|
||||||
|
kick: true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- remove placeholder members such as `_(): void`. An interface with any member is strict, an empty one accepts everything
|
||||||
|
|
||||||
|
## Other API changes
|
||||||
|
|
||||||
|
- `onCommand` callbacks get `args` as `string[]` (the generic argument type is gone)
|
||||||
|
- `RPCNativeClientNetworksEvents` is renamed to `RPCNativeClientNetworkEvents`
|
||||||
|
- internal types are no longer exported: `RPCEnvironmentResolved`, `RPCEventType`, `RPCEvents`, `RPCState`, `RPCStateRaw`, `RPCStateWeb`, `RPCStateWebRaw`. Use `RPCInstanceServer`, `RPCInstanceClient` or `RPCInstanceWebview` for the instance type
|
||||||
|
- server listeners get the player id from FiveM's `source`. Ids a client puts into the payload are ignored
|
||||||
|
- `emitClientEveryone` is one-way: clients run their listener but no longer send a response
|
||||||
+19
-17
@@ -1,22 +1,6 @@
|
|||||||
{
|
{
|
||||||
"workspaces": [
|
|
||||||
"rpc",
|
|
||||||
"shared-types"
|
|
||||||
],
|
|
||||||
"devDependencies": {
|
|
||||||
"@biomejs/biome": "^2.1.1",
|
|
||||||
"@types/bun": "^1.2.18"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"typescript": "^5"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"check": "bunx biome check",
|
|
||||||
"format": "bunx biome format --write"
|
|
||||||
},
|
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"license": "SEE LICENSE IN license.md",
|
||||||
"license": "CC0-1.0",
|
|
||||||
"author": "Entity Seven Group",
|
"author": "Entity Seven Group",
|
||||||
"contributors": [
|
"contributors": [
|
||||||
{
|
{
|
||||||
@@ -33,5 +17,23 @@
|
|||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/rilaxik/fivem-rpc.git"
|
"url": "https://github.com/rilaxik/fivem-rpc.git"
|
||||||
|
},
|
||||||
|
"workspaces": [
|
||||||
|
"rpc",
|
||||||
|
"shared-types"
|
||||||
|
],
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "bun run --filter '*' build",
|
||||||
|
"check": "oxfmt --check && oxlint",
|
||||||
|
"format": "oxfmt",
|
||||||
|
"format:check": "oxfmt --check",
|
||||||
|
"lint": "oxlint",
|
||||||
|
"typecheck": "bun run --filter '*' typecheck"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"oxfmt": "^0.70.0",
|
||||||
|
"oxlint": "^1.85.0",
|
||||||
|
"typescript": "^7.0.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
# FiveM RPC
|
||||||
|
|
||||||
|
Call FiveM server, client and NUI listeners like async functions: typed, with timeouts, no event ping-pong
|
||||||
|
|
||||||
|
## Motivation
|
||||||
|
|
||||||
|
The idea was to create an extensible package, with various features to simplify the development process and provide as much comfort as possible. Inspired by usage of [altv-xrpc](https://github.com/xxshady/altv-xrpc)
|
||||||
|
|
||||||
|
## Packages
|
||||||
|
|
||||||
|
| Package | Docs |
|
||||||
|
| ----------------------------------------------------- | -------------------------------------- |
|
||||||
|
| [`@entityseven/fivem-rpc`](rpc) | [API reference](rpc/readme.md) |
|
||||||
|
| [`@entityseven/fivem-rpc-shared-types`](shared-types) | [Typing setup](shared-types/readme.md) |
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm i @entityseven/fivem-rpc
|
||||||
|
pnpm add @entityseven/fivem-rpc
|
||||||
|
yarn add @entityseven/fivem-rpc
|
||||||
|
bun add @entityseven/fivem-rpc
|
||||||
|
```
|
||||||
|
|
||||||
|
Optional, for typed event names, arguments and results ([typing setup](shared-types/readme.md)):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm i -D @entityseven/fivem-rpc-shared-types
|
||||||
|
pnpm add -D @entityseven/fivem-rpc-shared-types
|
||||||
|
yarn add -D @entityseven/fivem-rpc-shared-types
|
||||||
|
bun add -d @entityseven/fivem-rpc-shared-types
|
||||||
|
```
|
||||||
|
|
||||||
|
Upgrading from 0.1: [migration guide](migration.md)
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
Create exactly one instance per environment and import it from your own module, not from the library. The client needs one even if it only relays between server and webview.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// server/rpc.ts
|
||||||
|
import { createRPC } from '@entityseven/fivem-rpc'
|
||||||
|
export const rpc = createRPC({ env: 'server' })
|
||||||
|
|
||||||
|
// client/rpc.ts
|
||||||
|
import { createRPC } from '@entityseven/fivem-rpc'
|
||||||
|
export const rpc = createRPC({ env: 'client' })
|
||||||
|
|
||||||
|
// webview/rpc.ts
|
||||||
|
import { createRPC } from '@entityseven/fivem-rpc'
|
||||||
|
export const rpc = createRPC({ env: 'webview' })
|
||||||
|
```
|
||||||
|
|
||||||
|
Listen on one side, emit from the other and await the listener's return value:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// server
|
||||||
|
rpc.onClient('ping', (player, message) => `pong: ${message} (from ${player})`)
|
||||||
|
|
||||||
|
// client
|
||||||
|
const reply = await rpc.emitServer('ping', 'hello')
|
||||||
|
```
|
||||||
|
|
||||||
|
All methods: [API reference](rpc/readme.md).
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Type-Safe Development: Eliminate runtime errors and enhance code reliability with comprehensive type safety
|
||||||
|
- All-in-one package: Communicate effortlessly between server, client and webview
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Issues and pull requests are very welcome
|
||||||
|
|
||||||
|
When the API changes, update the TSDoc, the [direction table](rpc/readme.md#directions) and the [agent skill](rpc/skills/fivem-rpc/SKILL.md)
|
||||||
|
|
||||||
|
Releases are published with `bun publish`, which replaces the `workspace:^` dependency on shared-types with its version (`npm publish` would not)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Licensed under the [Custom Attribution-NoDerivs Software License](license.md)
|
||||||
|
|
||||||
|
## Roadmap
|
||||||
|
|
||||||
|
- client observers to catch events between server and webview (subscribe-like behaviour)
|
||||||
|
- client observers to prevent events (middleware-like behaviour)
|
||||||
|
- player manager (transform player id to desired data straight from a listener)
|
||||||
|
|||||||
+51
-19
@@ -1,37 +1,69 @@
|
|||||||
{
|
{
|
||||||
"name": "@entityseven/fivem-rpc",
|
"name": "@entityseven/fivem-rpc",
|
||||||
"description": "FiveM RPC is an abstraction for events in GTA V FiveM servers in JS/TS",
|
"version": "1.0.0",
|
||||||
"version": "0.1.0",
|
"description": "Call FiveM server, client and NUI listeners like async functions: typed, with timeouts, no event ping-pong",
|
||||||
"main": "",
|
|
||||||
"types": "",
|
|
||||||
"files": [
|
|
||||||
"types/**/*",
|
|
||||||
"readme.md",
|
|
||||||
"license.md"
|
|
||||||
],
|
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"fivem-rpc",
|
"cfx",
|
||||||
"fivem-rpc-shared-types",
|
|
||||||
"fivem",
|
"fivem",
|
||||||
"gta"
|
"fivem-rpc",
|
||||||
|
"gta",
|
||||||
|
"nui",
|
||||||
|
"rpc",
|
||||||
|
"typescript"
|
||||||
],
|
],
|
||||||
"type": "module",
|
"license": "SEE LICENSE IN license.md",
|
||||||
"author": "Entity Seven Group",
|
"author": "Entity Seven Group",
|
||||||
"contributors": [
|
"contributors": [
|
||||||
{
|
{
|
||||||
"name": "Danya H",
|
"name": "Danya H",
|
||||||
"email": "[email protected]",
|
"email": "[email protected]",
|
||||||
"url": "https://github.com/rilaxik/"
|
"url": "https://github.com/rilaxik/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Oleksandr Honcharov",
|
||||||
|
"email": "[email protected]",
|
||||||
|
"url": "https://github.com/SashaGoncharov19/"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "Custom-Attribution-NoDerivs",
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/rilaxik/fivem-rpc.git",
|
||||||
|
"directory": "rpc"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist/**/*",
|
||||||
|
"skills/**/*",
|
||||||
|
"readme.md",
|
||||||
|
"license.md"
|
||||||
|
],
|
||||||
|
"type": "module",
|
||||||
|
"main": "./dist/index.cjs",
|
||||||
|
"module": "./dist/index.mjs",
|
||||||
|
"types": "./dist/index.d.cts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"import": "./dist/index.mjs",
|
||||||
|
"require": "./dist/index.cjs"
|
||||||
|
},
|
||||||
|
"./package.json": "./package.json"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsdown",
|
||||||
|
"prepublishOnly": "bun run build",
|
||||||
|
"typecheck": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@entityseven/fivem-rpc-shared-types": "workspace:^"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@microsoft/api-extractor": "^7.47.9",
|
"tsdown": "^0.23.0"
|
||||||
"@citizenfx/client": "^2.0.15015-1",
|
|
||||||
"@citizenfx/server": "^2.0.14862-1",
|
|
||||||
"tsup": "^8.3.0"
|
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"typescript": "^5"
|
"typescript": ">=5"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"typescript": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+436
@@ -0,0 +1,436 @@
|
|||||||
|
# FiveM RPC
|
||||||
|
|
||||||
|
Call FiveM server, client and NUI listeners like async functions: typed, with timeouts, no event ping-pong
|
||||||
|
|
||||||
|
Installation, quick start and package overview: [main readme](../readme.md). Typed events: [shared-types](../shared-types/readme.md). Upgrading from 0.1: [migration guide](../migration.md).
|
||||||
|
|
||||||
|
## Exports
|
||||||
|
|
||||||
|
Besides `createRPC` the package exports:
|
||||||
|
|
||||||
|
- `RPCError`, `RPCErrors`, `RPCErrorDetails` - see [Errors](#errors)
|
||||||
|
- `RPCConfig`, `RPCEnvironment` and the instance types `RPCInstanceServer`, `RPCInstanceClient`, `RPCInstanceWebview`
|
||||||
|
- native event types `RPCNativeServerEvents`, `RPCNativeClientEvents`, `RPCNativeClientNetworkEvents`, `RPCNativeClientNetworkEventsNames` and the lists `NATIVE_SERVER_EVENTS`, `NATIVE_CLIENT_EVENTS`, `NATIVE_CLIENT_NETWORK_EVENTS` accepted by the `onNative*` methods
|
||||||
|
|
||||||
|
## RPCConfig
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type RPCConfig = {
|
||||||
|
env: 'server' | 'client' | 'webview'
|
||||||
|
debug?: boolean // default false, logs every registration, call and incoming payload
|
||||||
|
timeout?: number // default 5000, ms to wait for a response, 0 disables it
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
An unknown `env` makes `createRPC` throw `RPCError` with code `RPCErrors.UNKNOWN_ENVIRONMENT`
|
||||||
|
|
||||||
|
## Errors
|
||||||
|
|
||||||
|
Every error from this library is an `RPCError`. `code` is one of `RPCErrors`, `message` says what to fix, and `details` names the call when the error comes from one
|
||||||
|
|
||||||
|
```ts
|
||||||
|
enum RPCErrors {
|
||||||
|
EVENT_NOT_REGISTERED = 'Event not registered',
|
||||||
|
UNKNOWN_NATIVE = 'Unknown native event',
|
||||||
|
UNKNOWN_ENVIRONMENT = 'Unknown environment',
|
||||||
|
TIMEOUT = 'Timed out waiting for response',
|
||||||
|
HANDLER_ERROR = 'Listener threw an error',
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example error
|
||||||
|
|
||||||
|
The server has no `onClient('buyItem', ...)` listener, so the call from the client rejects:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { RPCError, RPCErrors } from '@entityseven/fivem-rpc'
|
||||||
|
|
||||||
|
try {
|
||||||
|
await rpc.emitServer('buyItem', 'water')
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof RPCError && e.code === RPCErrors.EVENT_NOT_REGISTERED) {
|
||||||
|
e.message // 'No listener for "buyItem" on server. Register it with rpc.onClient("buyItem", ...) in server code.'
|
||||||
|
e.details // { event: 'buyItem', uuid: '<uuid>', from: 'client', to: 'server' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
### Directions
|
||||||
|
|
||||||
|
Every call goes from an `emit*` method in one environment to the matching `on*` listener in another. The last column is the [shared-types](../shared-types/readme.md) interface that types it
|
||||||
|
|
||||||
|
| From | Call | To | Listener | Typed by |
|
||||||
|
| ------- | ------------------------------------- | ----------- | ------------------------------------- | ------------------------- |
|
||||||
|
| server | `emitClient(player, event, ...args)` | client | `onServer` | `RPCEvents_ServerClient` |
|
||||||
|
| server | `emitClientEveryone(event, ...args)` | all clients | `onServer`, no response | `RPCEvents_ServerClient` |
|
||||||
|
| server | `emitWebview(player, event, ...args)` | webview | `onServer`, via client | `RPCEvents_ServerWebview` |
|
||||||
|
| server | `emitSelf(event, ...args)` | server | `onSelf` | `RPCEvents_Server` |
|
||||||
|
| client | `emitServer(event, ...args)` | server | `onClient`, player first | `RPCEvents_ClientServer` |
|
||||||
|
| client | `emitWebview(event, ...args)` | webview | `onClient` | `RPCEvents_ClientWebview` |
|
||||||
|
| client | `emitSelf(event, ...args)` | client | `onSelf` | `RPCEvents_Client` |
|
||||||
|
| webview | `emitServer(event, ...args)` | server | `onWebview`, player first, via client | `RPCEvents_WebviewServer` |
|
||||||
|
| webview | `emitClient(event, ...args)` | client | `onWebview` | `RPCEvents_WebviewClient` |
|
||||||
|
| webview | `emitSelf(event, ...args)` | webview | `onSelf` | `RPCEvents_Webview` |
|
||||||
|
|
||||||
|
Commands registered with `onCommand` are typed by `RPCCommands_Server` and `RPCCommands_Client`
|
||||||
|
|
||||||
|
### Routing
|
||||||
|
|
||||||
|
Server and client talk over FiveM network events, client and webview over NUI messages and NUI callbacks. Webview and server never talk directly: every call between them is relayed by the client of that player. So every client must run `createRPC({ env: 'client' })`, even with no listeners of its own, or those calls time out
|
||||||
|
|
||||||
|
### One listener per event
|
||||||
|
|
||||||
|
Each `on*` method keeps one listener per event name. Registering the same name again replaces the previous listener, `off*` removes it. Directions are separate: `onClient('x')` and `onWebview('x')` on the server do not replace each other
|
||||||
|
|
||||||
|
### Responses, errors and timeouts
|
||||||
|
|
||||||
|
- `emit*` resolves with the value the listener returns (promises are awaited)
|
||||||
|
- no listener on the target: the call rejects with `RPCErrors.EVENT_NOT_REGISTERED`
|
||||||
|
- the listener throws: the target logs the error with `console.error`, the call rejects with `RPCErrors.HANDLER_ERROR` and the original message
|
||||||
|
- no response within `RPCConfig.timeout` (default 5000 ms): the call rejects with `RPCErrors.TIMEOUT` and a late response is ignored. `timeout: 0` waits forever
|
||||||
|
- `emitSelf` calls the local listener directly, whatever it throws reaches the caller unchanged
|
||||||
|
- `emitClientEveryone` does not wait for clients: it resolves once sent, failures stay on each client (`console.error` for a throwing listener, the rest with `debug: true`)
|
||||||
|
|
||||||
|
### Player identity
|
||||||
|
|
||||||
|
Server listeners (`onClient`, `onWebview`) get the calling player's server id as the first argument. It comes from FiveM's `source`, never from the payload, so a client cannot pose as another player. Use it instead of player ids passed as arguments. A response to `emitClient` or `emitWebview` is only accepted from the player it was sent to
|
||||||
|
|
||||||
|
## Server ([source](src/core/server.ts))
|
||||||
|
|
||||||
|
### onClient
|
||||||
|
|
||||||
|
Listens to client event
|
||||||
|
|
||||||
|
```ts
|
||||||
|
rpc.onClient('clientServerEvent', (player, arg1, arg2, ...rest) => {
|
||||||
|
// logic
|
||||||
|
return someData // this will be forwarded back to caller
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### offClient
|
||||||
|
|
||||||
|
Stops listening to client event
|
||||||
|
|
||||||
|
```ts
|
||||||
|
rpc.offClient('clientServerEvent')
|
||||||
|
```
|
||||||
|
|
||||||
|
### emitClient
|
||||||
|
|
||||||
|
Sends event to specified client
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const response = await rpc.emitClient(playerServerId, 'serverClientEvent', someData)
|
||||||
|
// response will come from client listener with returned data
|
||||||
|
```
|
||||||
|
|
||||||
|
### emitClientEveryone
|
||||||
|
|
||||||
|
Sends event to all clients. One-way: clients run their listener but do not answer
|
||||||
|
|
||||||
|
```ts
|
||||||
|
await rpc.emitClientEveryone('serverClientEvent', someData)
|
||||||
|
```
|
||||||
|
|
||||||
|
### onWebview
|
||||||
|
|
||||||
|
Listens to webview event
|
||||||
|
|
||||||
|
```ts
|
||||||
|
rpc.onWebview('webviewServerEvent', (player, arg1, arg2, ...rest) => {
|
||||||
|
// logic
|
||||||
|
return someData // this will be forwarded back to caller
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### offWebview
|
||||||
|
|
||||||
|
Stops listening to webview event
|
||||||
|
|
||||||
|
```ts
|
||||||
|
rpc.offWebview('webviewServerEvent')
|
||||||
|
```
|
||||||
|
|
||||||
|
### emitWebview
|
||||||
|
|
||||||
|
Sends event to the webview of specified player
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const response = await rpc.emitWebview(playerServerId, 'serverWebviewEvent', someData)
|
||||||
|
// response will come from webview listener with returned data
|
||||||
|
```
|
||||||
|
|
||||||
|
### onSelf
|
||||||
|
|
||||||
|
Listens to server event
|
||||||
|
|
||||||
|
```ts
|
||||||
|
rpc.onSelf('serverEvent', (arg1, arg2, ...rest) => {
|
||||||
|
// logic
|
||||||
|
return someData // this will be forwarded back to caller
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### offSelf
|
||||||
|
|
||||||
|
Stops listening to server event
|
||||||
|
|
||||||
|
```ts
|
||||||
|
rpc.offSelf('serverEvent')
|
||||||
|
```
|
||||||
|
|
||||||
|
### emitSelf
|
||||||
|
|
||||||
|
Sends event to server
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const response = await rpc.emitSelf('serverEvent', someData)
|
||||||
|
// response will come from server listener with returned data
|
||||||
|
```
|
||||||
|
|
||||||
|
### onCommand
|
||||||
|
|
||||||
|
Registers chat command. `args` are the raw strings typed after the command, validate them yourself. With `restricted` set to `true` only players with the ACE permission `command.<name>` can use it (defaults to `false`)
|
||||||
|
|
||||||
|
```ts
|
||||||
|
rpc.onCommand('serverCommand', (player, args, rawCommand) => {
|
||||||
|
// logic
|
||||||
|
}, true /* restricted */)
|
||||||
|
```
|
||||||
|
|
||||||
|
### onNativeEvent
|
||||||
|
|
||||||
|
Listens to native server event ([reference](https://docs.fivem.net/docs/scripting-reference/events/server-events/))
|
||||||
|
|
||||||
|
```ts
|
||||||
|
rpc.onNativeEvent('playerJoining', (source, oldId) => {
|
||||||
|
// logic
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Client ([source](src/core/client.ts))
|
||||||
|
|
||||||
|
### onServer
|
||||||
|
|
||||||
|
Listens to server event
|
||||||
|
|
||||||
|
```ts
|
||||||
|
rpc.onServer('serverClientEvent', (arg1, arg2, ...rest) => {
|
||||||
|
// logic
|
||||||
|
return someData // this will be forwarded back to caller
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### offServer
|
||||||
|
|
||||||
|
Stops listening to server event
|
||||||
|
|
||||||
|
```ts
|
||||||
|
rpc.offServer('serverClientEvent')
|
||||||
|
```
|
||||||
|
|
||||||
|
### emitServer
|
||||||
|
|
||||||
|
Sends event to server
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const response = await rpc.emitServer('clientServerEvent', someData)
|
||||||
|
// response will come from server listener with returned data
|
||||||
|
```
|
||||||
|
|
||||||
|
### onWebview
|
||||||
|
|
||||||
|
Listens to webview event
|
||||||
|
|
||||||
|
```ts
|
||||||
|
rpc.onWebview('webviewClientEvent', (arg1, arg2, ...rest) => {
|
||||||
|
// logic
|
||||||
|
return someData // this will be forwarded back to caller
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### offWebview
|
||||||
|
|
||||||
|
Stops listening to webview event
|
||||||
|
|
||||||
|
```ts
|
||||||
|
rpc.offWebview('webviewClientEvent')
|
||||||
|
```
|
||||||
|
|
||||||
|
### emitWebview
|
||||||
|
|
||||||
|
Sends event to own webview
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const response = await rpc.emitWebview('clientWebviewEvent', someData)
|
||||||
|
// response will come from webview listener with returned data
|
||||||
|
```
|
||||||
|
|
||||||
|
### onSelf
|
||||||
|
|
||||||
|
Listens to client event
|
||||||
|
|
||||||
|
```ts
|
||||||
|
rpc.onSelf('clientEvent', (arg1, arg2, ...rest) => {
|
||||||
|
// logic
|
||||||
|
return someData // this will be forwarded back to caller
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### offSelf
|
||||||
|
|
||||||
|
Stops listening to client event
|
||||||
|
|
||||||
|
```ts
|
||||||
|
rpc.offSelf('clientEvent')
|
||||||
|
```
|
||||||
|
|
||||||
|
### emitSelf
|
||||||
|
|
||||||
|
Sends event to client
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const response = await rpc.emitSelf('clientEvent', someData)
|
||||||
|
// response will come from client listener with returned data
|
||||||
|
```
|
||||||
|
|
||||||
|
### onCommand
|
||||||
|
|
||||||
|
Registers chat command. `args` are the raw strings typed after the command, validate them yourself
|
||||||
|
|
||||||
|
```ts
|
||||||
|
rpc.onCommand('clientCommand', (player, args, rawCommand) => {
|
||||||
|
// logic
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### onNativeEvent
|
||||||
|
|
||||||
|
Listens to native client event ([reference](https://docs.fivem.net/docs/scripting-reference/events/client-events/))
|
||||||
|
|
||||||
|
```ts
|
||||||
|
rpc.onNativeEvent('entityDamaged', (victim, culprit, weapon, baseDamage) => {
|
||||||
|
// logic
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### onNativeNetworkEvent
|
||||||
|
|
||||||
|
Listens to native client network event ([reference](https://docs.fivem.net/docs/game-references/game-events/))
|
||||||
|
|
||||||
|
```ts
|
||||||
|
rpc.onNativeNetworkEvent('CEventShockingCarCrash', (entities, eventEntity, data) => {
|
||||||
|
// logic
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### setWebviewFocus
|
||||||
|
|
||||||
|
Sets or removes focus and cursor from own webview
|
||||||
|
|
||||||
|
```ts
|
||||||
|
rpc.setWebviewFocus(true /* focus */, true /* show cursor */)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Webview ([source](src/core/webview.ts))
|
||||||
|
|
||||||
|
### onClient
|
||||||
|
|
||||||
|
Listens to client event
|
||||||
|
|
||||||
|
```ts
|
||||||
|
rpc.onClient('clientWebviewEvent', (arg1, arg2, ...rest) => {
|
||||||
|
// logic
|
||||||
|
return someData // this will be forwarded back to caller
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### offClient
|
||||||
|
|
||||||
|
Stops listening to client event
|
||||||
|
|
||||||
|
```ts
|
||||||
|
rpc.offClient('clientWebviewEvent')
|
||||||
|
```
|
||||||
|
|
||||||
|
### emitClient
|
||||||
|
|
||||||
|
Sends event to own client
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const response = await rpc.emitClient('webviewClientEvent', someData)
|
||||||
|
// response will come from client listener with returned data
|
||||||
|
```
|
||||||
|
|
||||||
|
### onServer
|
||||||
|
|
||||||
|
Listens to server event
|
||||||
|
|
||||||
|
```ts
|
||||||
|
rpc.onServer('serverWebviewEvent', (arg1, arg2, ...rest) => {
|
||||||
|
// logic
|
||||||
|
return someData // this will be forwarded back to caller
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### offServer
|
||||||
|
|
||||||
|
Stops listening to server event
|
||||||
|
|
||||||
|
```ts
|
||||||
|
rpc.offServer('serverWebviewEvent')
|
||||||
|
```
|
||||||
|
|
||||||
|
### emitServer
|
||||||
|
|
||||||
|
Sends event to server
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const response = await rpc.emitServer('webviewServerEvent', someData)
|
||||||
|
// response will come from server listener with returned data
|
||||||
|
```
|
||||||
|
|
||||||
|
### onSelf
|
||||||
|
|
||||||
|
Listens to webview event
|
||||||
|
|
||||||
|
```ts
|
||||||
|
rpc.onSelf('webviewEvent', (arg1, arg2, ...rest) => {
|
||||||
|
// logic
|
||||||
|
return someData // this will be forwarded back to caller
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### offSelf
|
||||||
|
|
||||||
|
Stops listening to webview event
|
||||||
|
|
||||||
|
```ts
|
||||||
|
rpc.offSelf('webviewEvent')
|
||||||
|
```
|
||||||
|
|
||||||
|
### emitSelf
|
||||||
|
|
||||||
|
Sends event to webview
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const response = await rpc.emitSelf('webviewEvent', someData)
|
||||||
|
// response will come from webview listener with returned data
|
||||||
|
```
|
||||||
|
|
||||||
|
## Using with AI agents
|
||||||
|
|
||||||
|
The package ships an [Agent Skill](https://agentskills.io) with the directions, rules, typing and error fixes above. Copy it into your agent's skills folder, e.g. for Claude Code:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp -r node_modules/@entityseven/fivem-rpc/skills/fivem-rpc .claude/skills/
|
||||||
|
```
|
||||||
|
|
||||||
|
Copy it again after upgrading. Every method also carries TSDoc with its direction and matching listener
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Licensed under the [Custom Attribution-NoDerivs Software License](license.md)
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
---
|
||||||
|
name: fivem-rpc
|
||||||
|
description: Use when writing FiveM server, client or NUI (webview) code that calls between environments with @entityseven/fivem-rpc (createRPC, emitServer, onClient, emitWebview, onServer, ...), or when declaring its typed events in @entityseven/fivem-rpc-shared-types.
|
||||||
|
---
|
||||||
|
|
||||||
|
# @entityseven/fivem-rpc
|
||||||
|
|
||||||
|
Typed async calls between FiveM server, client and webview (NUI). Every `emit*` resolves with the return value of the matching `on*` listener in another environment
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
One instance per environment, created once in a local module and imported everywhere else:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// server/rpc.ts, same in client/rpc.ts with env 'client' and webview/rpc.ts with env 'webview'
|
||||||
|
import { createRPC } from '@entityseven/fivem-rpc'
|
||||||
|
export const rpc = createRPC({ env: 'server' }) // options: debug (false), timeout (5000 ms, 0 = none)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Directions
|
||||||
|
|
||||||
|
Method names are relative to the environment: `onClient` on the server listens to clients, `onClient` in the webview listens to its client
|
||||||
|
|
||||||
|
| From | Call | To | Listener | Typed by |
|
||||||
|
| ------- | ------------------------------------- | ----------- | ------------------------------------- | ------------------------- |
|
||||||
|
| server | `emitClient(player, event, ...args)` | client | `onServer` | `RPCEvents_ServerClient` |
|
||||||
|
| server | `emitClientEveryone(event, ...args)` | all clients | `onServer`, no response | `RPCEvents_ServerClient` |
|
||||||
|
| server | `emitWebview(player, event, ...args)` | webview | `onServer`, via client | `RPCEvents_ServerWebview` |
|
||||||
|
| server | `emitSelf(event, ...args)` | server | `onSelf` | `RPCEvents_Server` |
|
||||||
|
| client | `emitServer(event, ...args)` | server | `onClient`, player first | `RPCEvents_ClientServer` |
|
||||||
|
| client | `emitWebview(event, ...args)` | webview | `onClient` | `RPCEvents_ClientWebview` |
|
||||||
|
| client | `emitSelf(event, ...args)` | client | `onSelf` | `RPCEvents_Client` |
|
||||||
|
| webview | `emitServer(event, ...args)` | server | `onWebview`, player first, via client | `RPCEvents_WebviewServer` |
|
||||||
|
| webview | `emitClient(event, ...args)` | client | `onWebview` | `RPCEvents_WebviewClient` |
|
||||||
|
| webview | `emitSelf(event, ...args)` | webview | `onSelf` | `RPCEvents_Webview` |
|
||||||
|
|
||||||
|
Commands registered with `onCommand` are typed by `RPCCommands_Server` and `RPCCommands_Client`
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
- every `emit*` needs its listener registered in the target environment (see Directions), otherwise it rejects with `EVENT_NOT_REGISTERED`
|
||||||
|
- webview <-> server always goes through the player's client: the client must call `createRPC({ env: 'client' })` even with no listeners, otherwise those calls time out
|
||||||
|
- one listener per event and direction: registering the same name again replaces it, `off*` removes it
|
||||||
|
- always `await` or `.catch()` an `emit*`: it rejects with `RPCError`. `emitClientEveryone` is one-way and resolves once sent
|
||||||
|
- server `onClient` and `onWebview` listeners get the caller's server id first, taken from FiveM `source`. Use it, never trust player ids passed as arguments
|
||||||
|
- arguments and return values travel as JSON: pass plain data (no functions, class instances, `Map`, `Set`)
|
||||||
|
- never register FiveM events or NUI callbacks named `__rpc:*`, the library owns them
|
||||||
|
- `onNativeEvent` and `onNativeNetworkEvent` only accept names from `NATIVE_SERVER_EVENTS`, `NATIVE_CLIENT_EVENTS` and `NATIVE_CLIENT_NETWORK_EVENTS`. Use FiveM `on(name, cb)` for anything else
|
||||||
|
|
||||||
|
## Typing
|
||||||
|
|
||||||
|
Declare events in one `.d.ts` file included by the `tsconfig.json` of every environment. Without declarations every name, argument and result is `any`
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// shared/rpc.d.ts
|
||||||
|
import '@entityseven/fivem-rpc-shared-types' // required: without it the declaration replaces the module
|
||||||
|
|
||||||
|
declare module '@entityseven/fivem-rpc-shared-types' {
|
||||||
|
interface RPCEvents_WebviewServer {
|
||||||
|
// event name(arguments): value the listener returns
|
||||||
|
buyItem(item: string): boolean
|
||||||
|
}
|
||||||
|
interface RPCEvents_ServerClient {
|
||||||
|
itemBought(item: string): void
|
||||||
|
}
|
||||||
|
interface RPCCommands_Server {
|
||||||
|
ban: true // commands are keys, the value is not used
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Leave an interface empty to keep that direction untyped. Declare plain return values, listeners may still be `async`
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// server
|
||||||
|
rpc.onWebview('buyItem', async (player, item) => {
|
||||||
|
const ok = await chargePlayer(player, item)
|
||||||
|
if (ok) await rpc.emitClient(player, 'itemBought', item)
|
||||||
|
return ok
|
||||||
|
})
|
||||||
|
|
||||||
|
// client
|
||||||
|
rpc.onServer('itemBought', item => {
|
||||||
|
// update HUD, play a sound
|
||||||
|
})
|
||||||
|
|
||||||
|
// webview
|
||||||
|
const ok = await rpc.emitServer('buyItem', 'water')
|
||||||
|
```
|
||||||
|
|
||||||
|
## Errors
|
||||||
|
|
||||||
|
`RPCError.code` is one of `RPCErrors`, the message names the fix
|
||||||
|
|
||||||
|
| Code | Cause | Fix |
|
||||||
|
| ---------------------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
|
||||||
|
| `EVENT_NOT_REGISTERED` | no listener for the event on the target | register the listener from the Directions table on the target |
|
||||||
|
| `TIMEOUT` | no response within `timeout` (default 5000 ms) | listener must return or resolve; client needs `createRPC` for webview <-> server; raise `timeout` |
|
||||||
|
| `HANDLER_ERROR` | the listener threw, its message is included | fix the listener, the stack is logged with `console.error` on the target |
|
||||||
|
| `UNKNOWN_NATIVE` | `onNative*` name not in its `NATIVE_*` list | use FiveM `on(name, cb)` |
|
||||||
|
| `UNKNOWN_ENVIRONMENT` | `createRPC` got an `env` other than server, client, webview | fix `env` |
|
||||||
|
|
||||||
|
Debug with `createRPC({ env, debug: true })`: logs every registration, call and payload
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
import { Emitter, type Handler } from '../utils/emitter'
|
||||||
|
import {
|
||||||
|
handlerErrorMessage,
|
||||||
|
notRegisteredMessage,
|
||||||
|
RPCError,
|
||||||
|
} from '../utils/errors'
|
||||||
|
import { generateUUID, isRPCState, parse } from '../utils/funcs'
|
||||||
|
import { Pending } from '../utils/pending'
|
||||||
|
import {
|
||||||
|
type RPCConfig,
|
||||||
|
type RPCEnvironment,
|
||||||
|
RPCErrors,
|
||||||
|
type RPCState,
|
||||||
|
type RPCStateRaw,
|
||||||
|
} from '../utils/types'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* **Internal** Shared by `RPCInstanceServer`, `RPCInstanceClient` and
|
||||||
|
* `RPCInstanceWebview`.
|
||||||
|
*/
|
||||||
|
export class RPCInstanceBase {
|
||||||
|
protected readonly env: RPCEnvironment
|
||||||
|
protected readonly debug: boolean
|
||||||
|
protected readonly _emitterLocal = new Emitter()
|
||||||
|
protected readonly _pending: Pending
|
||||||
|
|
||||||
|
constructor(cfg: RPCConfig<RPCEnvironment>) {
|
||||||
|
this.env = cfg.env
|
||||||
|
this.debug = cfg.debug ?? false
|
||||||
|
this._pending = new Pending(cfg.timeout ?? 5000)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Registers `cb` for `event` on `emitter`; `method` is only for logs */
|
||||||
|
protected listen(
|
||||||
|
emitter: Emitter,
|
||||||
|
method: string,
|
||||||
|
event: string,
|
||||||
|
cb: Handler,
|
||||||
|
): this {
|
||||||
|
this.log(`${method} ${event}`)
|
||||||
|
emitter.on(event, cb)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Unregisters `cb` for `event` on `emitter`; `method` is only for logs */
|
||||||
|
protected unlisten(emitter: Emitter, method: string, event: string): this {
|
||||||
|
this.log(`${method} ${event}`)
|
||||||
|
emitter.off(event)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Builds an event payload sent from this environment to `to` */
|
||||||
|
protected request(
|
||||||
|
event: string,
|
||||||
|
to: RPCEnvironment,
|
||||||
|
args: unknown[],
|
||||||
|
player: number | null,
|
||||||
|
type: 'event' | 'broadcast' = 'event',
|
||||||
|
): RPCState {
|
||||||
|
return {
|
||||||
|
event,
|
||||||
|
uuid: generateUUID(),
|
||||||
|
calledFrom: this.env,
|
||||||
|
calledTo: to,
|
||||||
|
error: null,
|
||||||
|
data: args,
|
||||||
|
player,
|
||||||
|
type,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Calls a listener registered with `onSelf` in this environment */
|
||||||
|
protected emitLocal<R>(event: string, args: unknown[]): Promise<R> {
|
||||||
|
this.log(`emitSelf ${event}`)
|
||||||
|
if (!this._emitterLocal.has(event)) {
|
||||||
|
throw new RPCError(
|
||||||
|
RPCErrors.EVENT_NOT_REGISTERED,
|
||||||
|
notRegisteredMessage(event, this.env, this.env),
|
||||||
|
{ event, uuid: '', from: this.env, to: this.env },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return this._emitterLocal.emit<R>(event, ...args)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Settles the call waiting for `response`; ignores late or unexpected ones.
|
||||||
|
*
|
||||||
|
* @param peer - who sent the response (server: `source`)
|
||||||
|
*/
|
||||||
|
protected settle(response: RPCState, peer?: number): void {
|
||||||
|
const found = response.error
|
||||||
|
? this._pending.reject(
|
||||||
|
response.uuid,
|
||||||
|
RPCError.fromResponse(response),
|
||||||
|
peer,
|
||||||
|
)
|
||||||
|
: this._pending.resolve(response.uuid, response.data?.[0], peer)
|
||||||
|
|
||||||
|
if (!found) this.logIgnored(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses and validates an incoming payload. Anything else sent on the RPC
|
||||||
|
* channels (broken JSON, other shapes) is dropped: `null`.
|
||||||
|
*/
|
||||||
|
protected accept(input: RPCStateRaw | unknown): RPCState | null {
|
||||||
|
const payload =
|
||||||
|
typeof input === 'string'
|
||||||
|
? parse(input as RPCStateRaw)
|
||||||
|
: isRPCState(input)
|
||||||
|
? input
|
||||||
|
: null
|
||||||
|
|
||||||
|
if (!payload) {
|
||||||
|
this.log(`dropped invalid payload ${String(input).slice(0, 200)}`)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
this.log(
|
||||||
|
`accepted ${payload.type} ${payload.event} from ${payload.calledFrom}`,
|
||||||
|
)
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs the listener for `request` and builds the response to send back.
|
||||||
|
* Never throws: a missing listener or a thrown error ends up in `error`.
|
||||||
|
*
|
||||||
|
* @param prefix - arguments passed before the request data (server: player)
|
||||||
|
*/
|
||||||
|
protected async dispatch(
|
||||||
|
emitter: Emitter,
|
||||||
|
request: RPCState,
|
||||||
|
...prefix: unknown[]
|
||||||
|
): Promise<RPCState> {
|
||||||
|
if (!emitter.has(request.event)) {
|
||||||
|
return this.errorResponse(
|
||||||
|
request,
|
||||||
|
new RPCError(
|
||||||
|
RPCErrors.EVENT_NOT_REGISTERED,
|
||||||
|
notRegisteredMessage(request.event, this.env, request.calledFrom),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await emitter.emit(
|
||||||
|
request.event,
|
||||||
|
...prefix,
|
||||||
|
...(request.data ?? []),
|
||||||
|
)
|
||||||
|
return this.response(request, [data], null)
|
||||||
|
} catch (e) {
|
||||||
|
// keep the stack visible where the listener lives
|
||||||
|
console.error(e)
|
||||||
|
return this.errorResponse(
|
||||||
|
request,
|
||||||
|
new RPCError(
|
||||||
|
RPCErrors.HANDLER_ERROR,
|
||||||
|
handlerErrorMessage(request.event, this.env, e),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Builds the response to `request` carrying `error` */
|
||||||
|
protected errorResponse(request: RPCState, error: unknown): RPCState {
|
||||||
|
const rpcError =
|
||||||
|
error instanceof RPCError
|
||||||
|
? error
|
||||||
|
: new RPCError(
|
||||||
|
RPCErrors.HANDLER_ERROR,
|
||||||
|
handlerErrorMessage(request.event, this.env, error),
|
||||||
|
)
|
||||||
|
return this.response(request, null, {
|
||||||
|
code: rpcError.code,
|
||||||
|
message: rpcError.message,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Debug-only log, prefixed with the environment */
|
||||||
|
protected log(message: string): void {
|
||||||
|
if (this.debug) console.log(`[RPC]:${this.env}:${message}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected logIgnored(response: RPCState): void {
|
||||||
|
this.log(
|
||||||
|
`ignored response ${response.event} ${response.uuid} (no pending call, possibly timed out)`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private response(
|
||||||
|
request: RPCState,
|
||||||
|
data: RPCState['data'],
|
||||||
|
error: RPCState['error'],
|
||||||
|
): RPCState {
|
||||||
|
return {
|
||||||
|
event: request.event,
|
||||||
|
uuid: request.uuid,
|
||||||
|
calledFrom: this.env,
|
||||||
|
calledTo: request.calledFrom,
|
||||||
|
error,
|
||||||
|
data,
|
||||||
|
player: request.player,
|
||||||
|
type: 'response',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+224
-246
@@ -1,8 +1,8 @@
|
|||||||
/// <reference types="@citizenfx/client" />
|
|
||||||
|
|
||||||
import type * as s from '@entityseven/fivem-rpc-shared-types'
|
import type * as s from '@entityseven/fivem-rpc-shared-types'
|
||||||
|
|
||||||
import { Emitter } from '../utils/emitter'
|
import { Emitter } from '../utils/emitter'
|
||||||
import { generateUUID, parse, stringify, stringifyWeb } from '../utils/funcs'
|
import { RPCError, unknownNativeMessage } from '../utils/errors'
|
||||||
|
import { stringify, stringifyWeb } from '../utils/funcs'
|
||||||
import {
|
import {
|
||||||
NATIVE_CLIENT_EVENTS,
|
NATIVE_CLIENT_EVENTS,
|
||||||
NATIVE_CLIENT_NETWORK_EVENTS,
|
NATIVE_CLIENT_NETWORK_EVENTS,
|
||||||
@@ -12,38 +12,57 @@ import {
|
|||||||
RPCErrors,
|
RPCErrors,
|
||||||
RPCEvents,
|
RPCEvents,
|
||||||
type RPCNativeClientEvents,
|
type RPCNativeClientEvents,
|
||||||
type RPCNativeClientNetworksEvents,
|
type RPCNativeClientNetworkEvents,
|
||||||
type RPCState,
|
type RPCState,
|
||||||
type RPCStateRaw,
|
type RPCStateRaw,
|
||||||
type RPCStateWeb,
|
type RPCStateWeb,
|
||||||
} from '../utils/types'
|
} from '../utils/types'
|
||||||
import { Wrapper } from './wrapper'
|
import type {
|
||||||
|
RPCCommandName,
|
||||||
|
RPCEventArgs,
|
||||||
|
RPCEventName,
|
||||||
|
RPCEventResult,
|
||||||
|
RPCListener,
|
||||||
|
} from '../utils/typing'
|
||||||
|
import { RPCInstanceBase } from './base'
|
||||||
|
|
||||||
export class RPCInstanceClient extends Wrapper {
|
/**
|
||||||
|
* 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 _emitterServer: Emitter
|
||||||
private readonly _pendingServer: Emitter
|
|
||||||
private readonly _emitterWeb: Emitter
|
private readonly _emitterWeb: Emitter
|
||||||
private readonly _pendingWeb: Emitter
|
|
||||||
private readonly _pendingWebToServer: Emitter
|
|
||||||
|
|
||||||
constructor(props: RPCConfig<'client'>) {
|
constructor(props: RPCConfig<'client'>) {
|
||||||
super(props)
|
super(props)
|
||||||
|
|
||||||
this._emitterServer = new Emitter()
|
this._emitterServer = new Emitter()
|
||||||
this._pendingServer = new Emitter()
|
|
||||||
this._emitterWeb = new Emitter()
|
this._emitterWeb = new Emitter()
|
||||||
this._pendingWeb = new Emitter()
|
|
||||||
this._pendingWebToServer = new Emitter()
|
|
||||||
|
|
||||||
this.console.log('[RPC] Initialized Client')
|
console.log('[RPC] Initialized Client')
|
||||||
|
|
||||||
onNet(RPCEvents.LISTENER_SERVER, this._handleServer.bind(this))
|
onNet(RPCEvents.LISTENER_SERVER, this._handleServer.bind(this))
|
||||||
RegisterNuiCallbackType(RPCEvents.LISTENER_WEB)
|
RegisterNuiCallbackType(RPCEvents.LISTENER_WEB)
|
||||||
on(
|
on(
|
||||||
`__cfx_nui:${RPCEvents.LISTENER_WEB}`,
|
`__cfx_nui:${RPCEvents.LISTENER_WEB}`,
|
||||||
async (data: RPCState, callback: (res: unknown) => void) => {
|
async (data: unknown, callback: (res: unknown) => void) => {
|
||||||
const res = await this._handleWeb(data)
|
const payload = this.accept(data)
|
||||||
callback(res)
|
if (!payload) return callback({ status: 'invalid' })
|
||||||
|
|
||||||
|
try {
|
||||||
|
callback(await this._handleWeb(payload))
|
||||||
|
} catch (e) {
|
||||||
|
callback(this.errorResponse(payload, e))
|
||||||
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -51,40 +70,21 @@ export class RPCInstanceClient extends Wrapper {
|
|||||||
// ===== HANDLERS =====
|
// ===== HANDLERS =====
|
||||||
|
|
||||||
private async _handleServer(payloadRaw: RPCStateRaw) {
|
private async _handleServer(payloadRaw: RPCStateRaw) {
|
||||||
try {
|
const payload = this.accept(payloadRaw)
|
||||||
parse(payloadRaw)
|
if (!payload) return
|
||||||
} catch (e) {
|
|
||||||
throw new Error(RPCErrors.INVALID_DATA)
|
|
||||||
}
|
|
||||||
const payload = parse(payloadRaw)
|
|
||||||
|
|
||||||
if (this.debug) {
|
if (payload.type === 'event' || payload.type === 'broadcast') {
|
||||||
this.console.log(
|
if (payload.calledTo === 'client') {
|
||||||
`[RPC]:client:accepted ${payload.type} ${payload.event} from ${payload.calledFrom}`,
|
const response = await this.dispatch(this._emitterServer, payload)
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (payload.type === 'event') {
|
if (payload.type === 'event') {
|
||||||
if (payload.calledTo === 'client') {
|
|
||||||
this.verifyEvent(this._emitterServer, payload)
|
|
||||||
|
|
||||||
const responseData = await this._emitterServer.emit(
|
|
||||||
payload.event,
|
|
||||||
...(payload.data && payload.data.length > 0 ? payload.data : []),
|
|
||||||
)
|
|
||||||
|
|
||||||
const response: RPCState = {
|
|
||||||
event: payload.event,
|
|
||||||
uuid: payload.uuid,
|
|
||||||
calledFrom: 'client',
|
|
||||||
calledTo: 'server',
|
|
||||||
error: null,
|
|
||||||
data: [responseData],
|
|
||||||
player: payload.player,
|
|
||||||
type: 'response',
|
|
||||||
}
|
|
||||||
|
|
||||||
emitNet(RPCEvents.LISTENER_CLIENT, stringify(response))
|
emitNet(RPCEvents.LISTENER_CLIENT, stringify(response))
|
||||||
|
} else if (response.error) {
|
||||||
|
// nobody waits for a broadcast, so its failures only show up here
|
||||||
|
this.log(
|
||||||
|
`broadcast ${payload.event} failed: ${response.error.message}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (payload.calledTo === 'webview') {
|
if (payload.calledTo === 'webview') {
|
||||||
this._sendWebMessage({
|
this._sendWebMessage({
|
||||||
@@ -95,55 +95,36 @@ export class RPCInstanceClient extends Wrapper {
|
|||||||
}
|
}
|
||||||
if (payload.type === 'response') {
|
if (payload.type === 'response') {
|
||||||
if (payload.calledTo === 'client') {
|
if (payload.calledTo === 'client') {
|
||||||
await this._pendingServer.emit(
|
this.settle(payload)
|
||||||
payload.uuid,
|
|
||||||
...(payload.data && payload.data.length > 0 ? payload.data : []),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
if (payload.calledTo === 'webview') {
|
if (payload.calledTo === 'webview') {
|
||||||
await this._pendingWebToServer.emit(
|
// relayed webview -> server call: the webview gets the whole response
|
||||||
payload.uuid,
|
if (!this._pending.resolve(payload.uuid, payload)) {
|
||||||
...(payload.data && payload.data.length > 0 ? payload.data : []),
|
this.logIgnored(payload)
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _handleWeb(payload: RPCState): Promise<unknown> {
|
private async _handleWeb(payload: RPCState): Promise<unknown> {
|
||||||
if (this.debug) {
|
|
||||||
this.console.log(
|
|
||||||
`[RPC]:client:accepted ${payload.type} ${payload.event} from ${payload.calledFrom}`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (payload.type === 'event') {
|
if (payload.type === 'event') {
|
||||||
if (payload.calledTo === 'client') {
|
if (payload.calledTo === 'client') {
|
||||||
return await this._emitterWeb.emit(
|
return this.dispatch(this._emitterWeb, payload)
|
||||||
payload.event,
|
|
||||||
...(payload.data && payload.data.length > 0 ? payload.data : []),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
if (payload.calledTo === 'server') {
|
if (payload.calledTo === 'server') {
|
||||||
payload.player = GetPlayerServerId(PlayerId())
|
|
||||||
emitNet(RPCEvents.LISTENER_WEB, stringify(payload))
|
emitNet(RPCEvents.LISTENER_WEB, stringify(payload))
|
||||||
|
|
||||||
return new Promise(res => {
|
return this._pending.wait<RPCState>(payload)
|
||||||
this._pendingWebToServer.once(payload.uuid, res)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (payload.type === 'response') {
|
if (payload.type === 'response') {
|
||||||
if (payload.calledTo === 'client') {
|
if (payload.calledTo === 'client') {
|
||||||
await this._pendingWeb.emit(
|
this.settle(payload)
|
||||||
payload.uuid,
|
|
||||||
...(payload.data && payload.data.length > 0 ? payload.data : []),
|
|
||||||
)
|
|
||||||
|
|
||||||
return { status: 'ok' }
|
return { status: 'ok' }
|
||||||
}
|
}
|
||||||
if (payload.calledTo === 'server') {
|
if (payload.calledTo === 'server') {
|
||||||
payload.player = GetPlayerServerId(PlayerId())
|
|
||||||
emitNet(RPCEvents.LISTENER_WEB, stringify(payload))
|
emitNet(RPCEvents.LISTENER_WEB, stringify(payload))
|
||||||
|
|
||||||
return { status: 'ok' }
|
return { status: 'ok' }
|
||||||
@@ -154,240 +135,237 @@ export class RPCInstanceClient extends Wrapper {
|
|||||||
|
|
||||||
// ===== SERVER =====
|
// ===== SERVER =====
|
||||||
|
|
||||||
public onServer<
|
/**
|
||||||
EventName extends keyof s.RPCEvents_ServerClient,
|
* Listens for `emitClient` and `emitClientEveryone` calls from the server
|
||||||
CallbackArguments extends Parameters<s.RPCEvents_ServerClient[EventName]>,
|
* (server -> client). For `emitClientEveryone` the return value is not sent.
|
||||||
CallbackReturn extends ReturnType<s.RPCEvents_ServerClient[EventName]>,
|
*
|
||||||
|
* @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 extends RPCEventName<s.RPCEvents_ServerClient>>(
|
||||||
|
eventName: EventName,
|
||||||
|
cb: RPCListener<s.RPCEvents_ServerClient, EventName>,
|
||||||
|
): this {
|
||||||
|
return this.listen(this._emitterServer, 'onServer', eventName, cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Removes the `onServer` listener for `eventName` */
|
||||||
|
public offServer<EventName extends RPCEventName<s.RPCEvents_ServerClient>>(
|
||||||
|
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<s.RPCEvents_ClientServer>,
|
||||||
>(
|
>(
|
||||||
eventName: EventName,
|
eventName: EventName,
|
||||||
cb: (
|
...args: RPCEventArgs<s.RPCEvents_ClientServer, EventName>
|
||||||
...args: CallbackArguments
|
): Promise<RPCEventResult<s.RPCEvents_ClientServer, EventName>> {
|
||||||
) => Awaited<CallbackReturn> | Promise<Awaited<CallbackReturn>>,
|
const payload = this.request(eventName, 'server', args, null)
|
||||||
): this {
|
|
||||||
if (this.debug) {
|
|
||||||
this.console.log(`[RPC]:onServer ${eventName}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
this._emitterServer.on(eventName, cb)
|
|
||||||
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
public offServer<EventName extends keyof s.RPCEvents_ServerClient>(
|
|
||||||
eventName: EventName,
|
|
||||||
): this {
|
|
||||||
if (this.debug) {
|
|
||||||
this.console.log(`[RPC]:offServer ${eventName}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
this._emitterServer.off(eventName)
|
|
||||||
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
public async emitServer<
|
|
||||||
EventName extends keyof s.RPCEvents_ClientServer,
|
|
||||||
Arguments extends Parameters<s.RPCEvents_ClientServer[EventName]>,
|
|
||||||
Response extends ReturnType<s.RPCEvents_ClientServer[EventName]>,
|
|
||||||
>(eventName: EventName, ...args: Arguments): Promise<Awaited<Response>> {
|
|
||||||
const payload: RPCState = {
|
|
||||||
event: eventName,
|
|
||||||
uuid: generateUUID(),
|
|
||||||
calledFrom: 'client',
|
|
||||||
calledTo: 'server',
|
|
||||||
error: null,
|
|
||||||
data: args.length ? args : null,
|
|
||||||
player: GetPlayerServerId(PlayerId()),
|
|
||||||
type: 'event',
|
|
||||||
}
|
|
||||||
|
|
||||||
emitNet(RPCEvents.LISTENER_CLIENT, stringify(payload))
|
emitNet(RPCEvents.LISTENER_CLIENT, stringify(payload))
|
||||||
|
|
||||||
return new Promise<Awaited<Response>>(res => {
|
return this._pending.wait(payload)
|
||||||
this._pendingServer.once(payload.uuid, res)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== WEBVIEW =====
|
// ===== WEBVIEW =====
|
||||||
|
|
||||||
public onWebview<
|
/**
|
||||||
EventName extends keyof s.RPCEvents_WebviewClient,
|
* Listens for `emitClient` calls from this player's webview
|
||||||
CallbackArguments extends Parameters<s.RPCEvents_WebviewClient[EventName]>,
|
* (webview -> client).
|
||||||
CallbackReturn extends ReturnType<s.RPCEvents_WebviewClient[EventName]>,
|
*
|
||||||
|
* @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 extends RPCEventName<s.RPCEvents_WebviewClient>>(
|
||||||
|
eventName: EventName,
|
||||||
|
cb: RPCListener<s.RPCEvents_WebviewClient, EventName>,
|
||||||
|
): this {
|
||||||
|
return this.listen(this._emitterWeb, 'onWebview', eventName, cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Removes the `onWebview` listener for `eventName` */
|
||||||
|
public offWebview<EventName extends RPCEventName<s.RPCEvents_WebviewClient>>(
|
||||||
|
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<s.RPCEvents_ClientWebview>,
|
||||||
>(
|
>(
|
||||||
eventName: EventName,
|
eventName: EventName,
|
||||||
cb: (
|
...args: RPCEventArgs<s.RPCEvents_ClientWebview, EventName>
|
||||||
...args: CallbackArguments
|
): Promise<RPCEventResult<s.RPCEvents_ClientWebview, EventName>> {
|
||||||
) => Awaited<CallbackReturn> | Promise<Awaited<CallbackReturn>>,
|
const payload = this.request(eventName, 'webview', args, null)
|
||||||
): this {
|
|
||||||
if (this.debug) {
|
|
||||||
this.console.log(`[RPC]:onWebview ${eventName}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
this._emitterWeb.on(eventName, cb)
|
|
||||||
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
public offWebview<EventName extends keyof s.RPCEvents_WebviewClient>(
|
|
||||||
eventName: EventName,
|
|
||||||
): this {
|
|
||||||
if (this.debug) {
|
|
||||||
this.console.log(`[RPC]:offWebview ${eventName}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
this._emitterWeb.off(eventName)
|
|
||||||
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
public async emitWebview<
|
|
||||||
EventName extends keyof s.RPCEvents_ClientWebview,
|
|
||||||
Arguments extends Parameters<s.RPCEvents_ClientWebview[EventName]>,
|
|
||||||
Response extends ReturnType<s.RPCEvents_ClientWebview[EventName]>,
|
|
||||||
>(eventName: EventName, ...args: Arguments): Promise<Awaited<Response>> {
|
|
||||||
const payload: RPCState = {
|
|
||||||
event: eventName,
|
|
||||||
uuid: generateUUID(),
|
|
||||||
calledFrom: 'client',
|
|
||||||
calledTo: 'webview',
|
|
||||||
error: null,
|
|
||||||
data: args.length ? args : null,
|
|
||||||
player: PlayerId(),
|
|
||||||
type: 'event',
|
|
||||||
}
|
|
||||||
|
|
||||||
this._sendWebMessage({
|
this._sendWebMessage({
|
||||||
origin: RPCEvents.LISTENER_CLIENT,
|
origin: RPCEvents.LISTENER_CLIENT,
|
||||||
data: payload,
|
data: payload,
|
||||||
})
|
})
|
||||||
|
|
||||||
return new Promise<Awaited<Response>>(res => {
|
return this._pending.wait(payload)
|
||||||
this._pendingWeb.once(payload.uuid, res)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== SELF =====
|
// ===== SELF =====
|
||||||
|
|
||||||
public onSelf<
|
/**
|
||||||
EventName extends keyof s.RPCEvents_Client,
|
* Listens for `emitSelf` calls in this environment (client -> client).
|
||||||
CallbackArguments extends Parameters<s.RPCEvents_Client[EventName]>,
|
*
|
||||||
CallbackReturn extends ReturnType<s.RPCEvents_Client[EventName]>,
|
* @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 extends RPCEventName<s.RPCEvents_Client>>(
|
||||||
eventName: EventName,
|
eventName: EventName,
|
||||||
cb: (
|
cb: RPCListener<s.RPCEvents_Client, EventName>,
|
||||||
...args: CallbackArguments
|
|
||||||
) => Awaited<CallbackReturn> | Promise<Awaited<CallbackReturn>>,
|
|
||||||
): this {
|
): this {
|
||||||
if (this.debug) {
|
return this.listen(this._emitterLocal, 'onSelf', eventName, cb)
|
||||||
this.console.log(`[RPC]:onSelf ${eventName}`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this._emitterLocal.on(eventName, cb)
|
/** Removes the `onSelf` listener for `eventName` */
|
||||||
|
public offSelf<EventName extends RPCEventName<s.RPCEvents_Client>>(
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
public offSelf<EventName extends keyof s.RPCEvents_Client>(
|
|
||||||
eventName: EventName,
|
eventName: EventName,
|
||||||
): this {
|
): this {
|
||||||
if (this.debug) {
|
return this.unlisten(this._emitterLocal, 'offSelf', eventName)
|
||||||
this.console.log(`[RPC]:offSelf ${eventName}`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this._emitterLocal.off(eventName)
|
/**
|
||||||
|
* Calls this environment's own `onSelf` listener directly and resolves with
|
||||||
return this
|
* its return value. No timeout; errors thrown by the listener reach the caller
|
||||||
}
|
* unchanged.
|
||||||
|
*
|
||||||
public async emitSelf<
|
* @throws {@link RPCError} `EVENT_NOT_REGISTERED` if no `onSelf` listener exists
|
||||||
EventName extends keyof s.RPCEvents_Client,
|
*
|
||||||
Arguments extends Parameters<s.RPCEvents_Client[EventName]>,
|
* @example
|
||||||
Response extends ReturnType<s.RPCEvents_Client[EventName]>,
|
* const total = await rpc.emitSelf('add', 2, 3)
|
||||||
>(eventName: EventName, ...args: Arguments): Promise<Awaited<Response>> {
|
*/
|
||||||
const payload: RPCState = {
|
public async emitSelf<EventName extends RPCEventName<s.RPCEvents_Client>>(
|
||||||
event: eventName,
|
eventName: EventName,
|
||||||
uuid: generateUUID(),
|
...args: RPCEventArgs<s.RPCEvents_Client, EventName>
|
||||||
calledFrom: 'client',
|
): Promise<RPCEventResult<s.RPCEvents_Client, EventName>> {
|
||||||
calledTo: 'client',
|
return this.emitLocal(eventName, args)
|
||||||
error: null,
|
|
||||||
data: args.length ? args : null,
|
|
||||||
player: null,
|
|
||||||
type: 'event',
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.debug) {
|
|
||||||
this.console.log(
|
|
||||||
`[RPC]:accepted ${payload.event} from ${payload.calledFrom}`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.verifyEvent(this._emitterLocal, payload)
|
|
||||||
|
|
||||||
return await this._emitterLocal.emit<Awaited<Response>>(
|
|
||||||
payload.event,
|
|
||||||
...(payload.data && payload.data.length > 0 ? payload.data : []),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== OTHER =====
|
// ===== OTHER =====
|
||||||
|
|
||||||
public onCommand<
|
/**
|
||||||
CommandName extends s.RPCCommands_Client,
|
* Registers a chat command (FiveM `RegisterCommand`).
|
||||||
CallbackArguments extends unknown[],
|
*
|
||||||
>(
|
* @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<CommandName extends RPCCommandName<s.RPCCommands_Client>>(
|
||||||
command: CommandName,
|
command: CommandName,
|
||||||
cb: (player: number, args: CallbackArguments, commandRaw: string) => void,
|
cb: (player: number, args: string[], rawCommand: string) => void,
|
||||||
): this {
|
): this {
|
||||||
if (this.debug) {
|
this.log(`onCommand ${command}`)
|
||||||
this.console.log(`[RPC]:onCommand ${command}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
RegisterCommand(command, cb, false)
|
RegisterCommand(command, cb, false)
|
||||||
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
public onNativeEvent<
|
/**
|
||||||
EventName extends keyof RPCNativeClientEvents,
|
* Listens to a native FiveM client event, e.g. `entityDamaged`.
|
||||||
CallbackArguments extends Parameters<RPCNativeClientEvents[EventName]>,
|
*
|
||||||
>(eventName: EventName, cb: (...args: CallbackArguments) => void): this {
|
* @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 extends keyof RPCNativeClientEvents>(
|
||||||
|
eventName: EventName,
|
||||||
|
cb: (...args: Parameters<RPCNativeClientEvents[EventName]>) => void,
|
||||||
|
): this {
|
||||||
if (!NATIVE_CLIENT_EVENTS.includes(eventName)) {
|
if (!NATIVE_CLIENT_EVENTS.includes(eventName)) {
|
||||||
throw new Error(RPCErrors.UNKNOWN_NATIVE)
|
throw new RPCError(
|
||||||
|
RPCErrors.UNKNOWN_NATIVE,
|
||||||
|
unknownNativeMessage(eventName, 'NATIVE_CLIENT_EVENTS'),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.debug) {
|
this.log(`onNativeEvent ${eventName}`)
|
||||||
this.console.log(`[RPC]:onNativeEvent ${eventName}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
on(eventName, cb)
|
on(eventName, cb)
|
||||||
|
|
||||||
return this
|
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<
|
public onNativeNetworkEvent<
|
||||||
EventName extends keyof RPCNativeClientNetworksEvents,
|
EventName extends keyof RPCNativeClientNetworkEvents,
|
||||||
CallbackArguments extends Parameters<
|
>(
|
||||||
RPCNativeClientNetworksEvents[EventName]
|
eventName: EventName,
|
||||||
>,
|
cb: (...args: Parameters<RPCNativeClientNetworkEvents[EventName]>) => void,
|
||||||
>(eventName: EventName, cb: (...args: CallbackArguments) => void): this {
|
): this {
|
||||||
if (!NATIVE_CLIENT_NETWORK_EVENTS.includes(eventName)) {
|
if (!NATIVE_CLIENT_NETWORK_EVENTS.includes(eventName)) {
|
||||||
throw new Error(RPCErrors.UNKNOWN_NATIVE)
|
throw new RPCError(
|
||||||
|
RPCErrors.UNKNOWN_NATIVE,
|
||||||
|
unknownNativeMessage(eventName, 'NATIVE_CLIENT_NETWORK_EVENTS'),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.debug) {
|
this.log(`onNativeNetworkEvent ${eventName}`)
|
||||||
this.console.log(`[RPC]:onNativeNetworkEvent ${eventName}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
on(eventName, cb)
|
on(eventName, cb)
|
||||||
|
|
||||||
return this
|
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 {
|
public setWebviewFocus(hasFocus: boolean, hasCursor: boolean): this {
|
||||||
if (this.debug) {
|
this.log(`setWebviewFocus ${hasFocus} ${hasCursor}`)
|
||||||
this.console.log(`[RPC]:setWebviewFocus ${hasFocus} ${hasCursor}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
SetNuiFocus(hasFocus, hasCursor)
|
SetNuiFocus(hasFocus, hasCursor)
|
||||||
|
|
||||||
|
|||||||
+196
-271
@@ -1,376 +1,301 @@
|
|||||||
/// <reference types="@citizenfx/server" />
|
|
||||||
|
|
||||||
import type * as s from '@entityseven/fivem-rpc-shared-types'
|
import type * as s from '@entityseven/fivem-rpc-shared-types'
|
||||||
|
|
||||||
import { Emitter } from '../utils/emitter'
|
import { Emitter } from '../utils/emitter'
|
||||||
import { generateUUID, parse, stringify } from '../utils/funcs'
|
import { RPCError, unknownNativeMessage } from '../utils/errors'
|
||||||
|
import { stringify } from '../utils/funcs'
|
||||||
import { NATIVE_SERVER_EVENTS } from '../utils/native'
|
import { NATIVE_SERVER_EVENTS } from '../utils/native'
|
||||||
import {
|
import {
|
||||||
type RPCConfig,
|
type RPCConfig,
|
||||||
RPCErrors,
|
RPCErrors,
|
||||||
RPCEvents,
|
RPCEvents,
|
||||||
type RPCNativeServerEvents,
|
type RPCNativeServerEvents,
|
||||||
type RPCState,
|
|
||||||
type RPCStateRaw,
|
type RPCStateRaw,
|
||||||
} from '../utils/types'
|
} from '../utils/types'
|
||||||
import { Wrapper } from './wrapper'
|
import type {
|
||||||
|
RPCCommandName,
|
||||||
|
RPCEventArgs,
|
||||||
|
RPCEventName,
|
||||||
|
RPCEventResult,
|
||||||
|
RPCListener,
|
||||||
|
} from '../utils/typing'
|
||||||
|
import { RPCInstanceBase } from './base'
|
||||||
|
|
||||||
export class RPCInstanceServer extends Wrapper {
|
/**
|
||||||
|
* 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 _emitterClient: Emitter
|
||||||
private readonly _pendingClient: Emitter
|
|
||||||
private readonly _emitterWeb: Emitter
|
private readonly _emitterWeb: Emitter
|
||||||
private readonly _pendingWeb: Emitter
|
|
||||||
|
|
||||||
constructor(props: RPCConfig<'server'>) {
|
constructor(props: RPCConfig<'server'>) {
|
||||||
super(props)
|
super(props)
|
||||||
|
|
||||||
this._emitterClient = new Emitter()
|
this._emitterClient = new Emitter()
|
||||||
this._pendingClient = new Emitter()
|
|
||||||
this._emitterWeb = new Emitter()
|
this._emitterWeb = new Emitter()
|
||||||
this._pendingWeb = new Emitter()
|
|
||||||
|
|
||||||
this.console.log('[RPC] Initialized Server')
|
console.log('[RPC] Initialized Server')
|
||||||
|
|
||||||
onNet(RPCEvents.LISTENER_CLIENT, this._handleClient.bind(this))
|
// `source` must be read synchronously, before any await
|
||||||
onNet(RPCEvents.LISTENER_WEB, this._handleWeb.bind(this))
|
onNet(RPCEvents.LISTENER_CLIENT, (raw: RPCStateRaw) =>
|
||||||
|
this._handle(raw, source, 'client', this._emitterClient),
|
||||||
|
)
|
||||||
|
onNet(RPCEvents.LISTENER_WEB, (raw: RPCStateRaw) =>
|
||||||
|
this._handle(raw, source, 'webview', this._emitterWeb),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== HANDLERS =====
|
// ===== HANDLERS =====
|
||||||
|
|
||||||
private async _handleClient(payloadRaw: RPCStateRaw) {
|
/**
|
||||||
try {
|
* Handles a payload from a client or its webview.
|
||||||
parse(payloadRaw)
|
*
|
||||||
} catch (e) {
|
* @param player - FiveM `source` of the net event: the only trusted player id,
|
||||||
throw new Error(RPCErrors.INVALID_DATA)
|
* whatever the payload claims
|
||||||
}
|
*/
|
||||||
const payload = parse(payloadRaw)
|
private async _handle(
|
||||||
|
payloadRaw: RPCStateRaw,
|
||||||
|
player: number,
|
||||||
|
from: 'client' | 'webview',
|
||||||
|
emitter: Emitter,
|
||||||
|
) {
|
||||||
|
const payload = this.accept(payloadRaw)
|
||||||
|
if (!payload || payload.calledFrom !== from) return
|
||||||
|
|
||||||
if (this.debug) {
|
// not sent by a player, e.g. a server-side trigger of the RPC channel
|
||||||
this.console.log(
|
if (!(player > 0)) {
|
||||||
`[RPC]:server:accepted ${payload.type} ${payload.event} from ${payload.calledFrom}`,
|
this.log(`dropped ${payload.event}: no player source`)
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (payload.calledFrom === 'client') {
|
|
||||||
if (payload.type === 'event') {
|
|
||||||
this.verifyEvent(this._emitterClient, payload)
|
|
||||||
if (payload.player === null || payload.player === -1) {
|
|
||||||
payload.error = RPCErrors.NO_PLAYER
|
|
||||||
this.triggerError(payload)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
payload.player = player
|
||||||
|
|
||||||
const responseData = await this._emitterClient.emit(
|
|
||||||
payload.event,
|
|
||||||
payload.player,
|
|
||||||
...(payload.data && payload.data.length > 0 ? payload.data : []),
|
|
||||||
)
|
|
||||||
|
|
||||||
const response: RPCState = {
|
|
||||||
event: payload.event,
|
|
||||||
uuid: payload.uuid,
|
|
||||||
calledFrom: 'server',
|
|
||||||
calledTo: 'client',
|
|
||||||
error: null,
|
|
||||||
data: [responseData],
|
|
||||||
player: payload.player,
|
|
||||||
type: 'response',
|
|
||||||
}
|
|
||||||
|
|
||||||
emitNet(RPCEvents.LISTENER_SERVER, response.player, stringify(response))
|
|
||||||
}
|
|
||||||
if (payload.type === 'response') {
|
|
||||||
await this._pendingClient.emit(
|
|
||||||
payload.uuid,
|
|
||||||
...(payload.data && payload.data.length > 0 ? payload.data : []),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _handleWeb(payloadRaw: RPCStateRaw) {
|
|
||||||
try {
|
|
||||||
parse(payloadRaw)
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error(RPCErrors.INVALID_DATA)
|
|
||||||
}
|
|
||||||
const payload = parse(payloadRaw)
|
|
||||||
|
|
||||||
if (this.debug) {
|
|
||||||
this.console.log(
|
|
||||||
`[RPC]:server:accepted ${payload.type} ${payload.event} from ${payload.calledFrom}`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (payload.calledFrom === 'webview') {
|
|
||||||
if (payload.type === 'event') {
|
if (payload.type === 'event') {
|
||||||
this.verifyEvent(this._emitterWeb, payload)
|
const response = await this.dispatch(emitter, payload, player)
|
||||||
if (payload.player === null || payload.player === -1) {
|
emitNet(RPCEvents.LISTENER_SERVER, player, stringify(response))
|
||||||
payload.error = RPCErrors.NO_PLAYER
|
} else if (payload.type === 'response') {
|
||||||
this.triggerError(payload)
|
this.settle(payload, player)
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const responseData = await this._emitterWeb.emit(
|
|
||||||
payload.event,
|
|
||||||
payload.player,
|
|
||||||
...(payload.data && payload.data.length > 0 ? payload.data : []),
|
|
||||||
)
|
|
||||||
|
|
||||||
const response: RPCState = {
|
|
||||||
event: payload.event,
|
|
||||||
uuid: payload.uuid,
|
|
||||||
calledFrom: 'server',
|
|
||||||
calledTo: 'webview',
|
|
||||||
error: null,
|
|
||||||
data: [responseData],
|
|
||||||
player: payload.player,
|
|
||||||
type: 'response',
|
|
||||||
}
|
|
||||||
|
|
||||||
emitNet(RPCEvents.LISTENER_SERVER, response.player, stringify(response))
|
|
||||||
}
|
|
||||||
if (payload.type === 'response') {
|
|
||||||
await this._pendingWeb.emit(
|
|
||||||
payload.uuid,
|
|
||||||
...(payload.data && payload.data.length > 0 ? payload.data : []),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== CLIENT =====
|
// ===== CLIENT =====
|
||||||
|
|
||||||
public onClient<
|
/**
|
||||||
EventName extends keyof s.RPCEvents_ClientServer,
|
* Listens for `emitServer` calls from clients (client -> server).
|
||||||
CallbackArguments extends Parameters<s.RPCEvents_ClientServer[EventName]>,
|
*
|
||||||
CallbackReturn extends ReturnType<s.RPCEvents_ClientServer[EventName]>,
|
* @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 extends RPCEventName<s.RPCEvents_ClientServer>>(
|
||||||
eventName: EventName,
|
eventName: EventName,
|
||||||
cb: (
|
cb: RPCListener<s.RPCEvents_ClientServer, EventName, [player: number]>,
|
||||||
player: number,
|
|
||||||
...args: CallbackArguments
|
|
||||||
) => Awaited<CallbackReturn> | Promise<Awaited<CallbackReturn>>,
|
|
||||||
): this {
|
): this {
|
||||||
if (this.debug) {
|
return this.listen(this._emitterClient, 'onClient', eventName, cb)
|
||||||
this.console.log(`[RPC]:onClient ${eventName}`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this._emitterClient.on(eventName, cb)
|
/** Removes the `onClient` listener for `eventName` */
|
||||||
|
public offClient<EventName extends RPCEventName<s.RPCEvents_ClientServer>>(
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
public offClient<EventName extends keyof s.RPCEvents_ClientServer>(
|
|
||||||
eventName: EventName,
|
eventName: EventName,
|
||||||
): this {
|
): this {
|
||||||
if (this.debug) {
|
return this.unlisten(this._emitterClient, 'offClient', eventName)
|
||||||
this.console.log(`[RPC]:offClient ${eventName}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
this._emitterClient.off(eventName)
|
|
||||||
|
|
||||||
return this
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<
|
public async emitClient<
|
||||||
EventName extends keyof s.RPCEvents_ServerClient,
|
EventName extends RPCEventName<s.RPCEvents_ServerClient>,
|
||||||
Arguments extends Parameters<s.RPCEvents_ServerClient[EventName]>,
|
|
||||||
Response extends ReturnType<s.RPCEvents_ServerClient[EventName]>,
|
|
||||||
>(
|
>(
|
||||||
player: number,
|
player: number,
|
||||||
eventName: EventName,
|
eventName: EventName,
|
||||||
...args: Arguments
|
...args: RPCEventArgs<s.RPCEvents_ServerClient, EventName>
|
||||||
): Promise<Awaited<Response>> {
|
): Promise<RPCEventResult<s.RPCEvents_ServerClient, EventName>> {
|
||||||
const payload: RPCState = {
|
const payload = this.request(eventName, 'client', args, player)
|
||||||
event: eventName,
|
|
||||||
uuid: generateUUID(),
|
|
||||||
calledFrom: 'server',
|
|
||||||
calledTo: 'client',
|
|
||||||
error: null,
|
|
||||||
data: args.length ? args : null,
|
|
||||||
player: player,
|
|
||||||
type: 'event',
|
|
||||||
}
|
|
||||||
|
|
||||||
emitNet(RPCEvents.LISTENER_SERVER, player, stringify(payload))
|
emitNet(RPCEvents.LISTENER_SERVER, player, stringify(payload))
|
||||||
|
|
||||||
return new Promise<Awaited<Response>>(res => {
|
return this._pending.wait(payload, player)
|
||||||
this._pendingClient.once(payload.uuid, res)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<
|
public async emitClientEveryone<
|
||||||
EventName extends keyof s.RPCEvents_ServerClient,
|
EventName extends RPCEventName<s.RPCEvents_ServerClient>,
|
||||||
Arguments extends Parameters<s.RPCEvents_ServerClient[EventName]>,
|
>(
|
||||||
>(eventName: EventName, ...args: Arguments): Promise<void> {
|
eventName: EventName,
|
||||||
const payload: RPCState = {
|
...args: RPCEventArgs<s.RPCEvents_ServerClient, EventName>
|
||||||
event: eventName,
|
): Promise<void> {
|
||||||
uuid: generateUUID(),
|
const payload = this.request(eventName, 'client', args, -1, 'broadcast')
|
||||||
calledFrom: 'server',
|
|
||||||
calledTo: 'client',
|
|
||||||
error: null,
|
|
||||||
data: args.length ? args : null,
|
|
||||||
player: -1,
|
|
||||||
type: 'event',
|
|
||||||
}
|
|
||||||
|
|
||||||
emitNet(RPCEvents.LISTENER_SERVER, -1, stringify(payload))
|
emitNet(RPCEvents.LISTENER_SERVER, -1, stringify(payload))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== WEBVIEW =====
|
// ===== WEBVIEW =====
|
||||||
|
|
||||||
public onWebview<
|
/**
|
||||||
EventName extends keyof s.RPCEvents_WebviewServer,
|
* Listens for `emitServer` calls from webviews (webview -> server, relayed
|
||||||
CallbackArguments extends Parameters<s.RPCEvents_WebviewServer[EventName]>,
|
* by the player's client).
|
||||||
CallbackReturn extends ReturnType<s.RPCEvents_WebviewServer[EventName]>,
|
*
|
||||||
>(
|
* @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 extends RPCEventName<s.RPCEvents_WebviewServer>>(
|
||||||
eventName: EventName,
|
eventName: EventName,
|
||||||
cb: (
|
cb: RPCListener<s.RPCEvents_WebviewServer, EventName, [player: number]>,
|
||||||
player: number,
|
|
||||||
...args: CallbackArguments
|
|
||||||
) => Awaited<CallbackReturn> | Promise<Awaited<CallbackReturn>>,
|
|
||||||
): this {
|
): this {
|
||||||
if (this.debug) {
|
return this.listen(this._emitterWeb, 'onWebview', eventName, cb)
|
||||||
this.console.log(`[RPC]:onWebview ${eventName}`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this._emitterWeb.on(eventName, cb)
|
/** Removes the `onWebview` listener for `eventName` */
|
||||||
|
public offWebview<EventName extends RPCEventName<s.RPCEvents_WebviewServer>>(
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
public offWebview<EventName extends keyof s.RPCEvents_WebviewServer>(
|
|
||||||
eventName: EventName,
|
eventName: EventName,
|
||||||
): this {
|
): this {
|
||||||
if (this.debug) {
|
return this.unlisten(this._emitterWeb, 'offWebview', eventName)
|
||||||
this.console.log(`[RPC]:offWebview ${eventName}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
this._emitterWeb.off(eventName)
|
|
||||||
|
|
||||||
return this
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<
|
public async emitWebview<
|
||||||
EventName extends keyof s.RPCEvents_ServerWebview,
|
EventName extends RPCEventName<s.RPCEvents_ServerWebview>,
|
||||||
Arguments extends Parameters<s.RPCEvents_ServerWebview[EventName]>,
|
|
||||||
Response extends ReturnType<s.RPCEvents_ServerWebview[EventName]>,
|
|
||||||
>(
|
>(
|
||||||
player: number,
|
player: number,
|
||||||
eventName: EventName,
|
eventName: EventName,
|
||||||
...args: Arguments
|
...args: RPCEventArgs<s.RPCEvents_ServerWebview, EventName>
|
||||||
): Promise<Awaited<Response>> {
|
): Promise<RPCEventResult<s.RPCEvents_ServerWebview, EventName>> {
|
||||||
const payload: RPCState = {
|
const payload = this.request(eventName, 'webview', args, player)
|
||||||
event: eventName,
|
|
||||||
uuid: generateUUID(),
|
|
||||||
calledFrom: 'server',
|
|
||||||
calledTo: 'webview',
|
|
||||||
error: null,
|
|
||||||
data: args.length ? args : null,
|
|
||||||
player: player,
|
|
||||||
type: 'event',
|
|
||||||
}
|
|
||||||
|
|
||||||
emitNet(RPCEvents.LISTENER_SERVER, player, stringify(payload))
|
emitNet(RPCEvents.LISTENER_SERVER, player, stringify(payload))
|
||||||
|
|
||||||
return new Promise<Awaited<Response>>(res => {
|
return this._pending.wait(payload, player)
|
||||||
this._pendingWeb.once(payload.uuid, res)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== SELF =====
|
// ===== SELF =====
|
||||||
|
|
||||||
public onSelf<
|
/**
|
||||||
EventName extends keyof s.RPCEvents_Server,
|
* Listens for `emitSelf` calls in this environment (server -> server).
|
||||||
CallbackArguments extends Parameters<s.RPCEvents_Server[EventName]>,
|
*
|
||||||
CallbackReturn extends ReturnType<s.RPCEvents_Server[EventName]>,
|
* @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 extends RPCEventName<s.RPCEvents_Server>>(
|
||||||
eventName: EventName,
|
eventName: EventName,
|
||||||
cb: (
|
cb: RPCListener<s.RPCEvents_Server, EventName>,
|
||||||
...args: CallbackArguments
|
|
||||||
) => Awaited<CallbackReturn> | Promise<Awaited<CallbackReturn>>,
|
|
||||||
): this {
|
): this {
|
||||||
if (this.debug) {
|
return this.listen(this._emitterLocal, 'onSelf', eventName, cb)
|
||||||
this.console.log(`[RPC]:onSelf ${eventName}`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this._emitterLocal.on(eventName, cb)
|
/** Removes the `onSelf` listener for `eventName` */
|
||||||
|
public offSelf<EventName extends RPCEventName<s.RPCEvents_Server>>(
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
public offSelf<EventName extends keyof s.RPCEvents_Server>(
|
|
||||||
eventName: EventName,
|
eventName: EventName,
|
||||||
): this {
|
): this {
|
||||||
if (this.debug) {
|
return this.unlisten(this._emitterLocal, 'offSelf', eventName)
|
||||||
this.console.log(`[RPC]:offSelf ${eventName}`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this._emitterLocal.off(eventName)
|
/**
|
||||||
|
* Calls this environment's own `onSelf` listener directly and resolves with
|
||||||
return this
|
* its return value. No timeout; errors thrown by the listener reach the caller
|
||||||
}
|
* unchanged.
|
||||||
|
*
|
||||||
public async emitSelf<
|
* @throws {@link RPCError} `EVENT_NOT_REGISTERED` if no `onSelf` listener exists
|
||||||
EventName extends keyof s.RPCEvents_Server,
|
*
|
||||||
Arguments extends Parameters<s.RPCEvents_Server[EventName]>,
|
* @example
|
||||||
Response extends ReturnType<s.RPCEvents_Server[EventName]>,
|
* const total = await rpc.emitSelf('add', 2, 3)
|
||||||
>(eventName: EventName, ...args: Arguments): Promise<Awaited<Response>> {
|
*/
|
||||||
const payload: RPCState = {
|
public async emitSelf<EventName extends RPCEventName<s.RPCEvents_Server>>(
|
||||||
event: eventName,
|
eventName: EventName,
|
||||||
uuid: generateUUID(),
|
...args: RPCEventArgs<s.RPCEvents_Server, EventName>
|
||||||
calledFrom: 'server',
|
): Promise<RPCEventResult<s.RPCEvents_Server, EventName>> {
|
||||||
calledTo: 'server',
|
return this.emitLocal(eventName, args)
|
||||||
error: null,
|
|
||||||
data: args.length ? args : null,
|
|
||||||
player: null,
|
|
||||||
type: 'event',
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.debug) {
|
|
||||||
this.console.log(
|
|
||||||
`[RPC]:accepted ${payload.event} from ${payload.calledFrom}`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.verifyEvent(this._emitterLocal, payload)
|
|
||||||
|
|
||||||
return await this._emitterLocal.emit<Awaited<Response>>(
|
|
||||||
payload.event,
|
|
||||||
...(payload.data && payload.data.length > 0 ? payload.data : []),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== OTHER =====
|
// ===== OTHER =====
|
||||||
|
|
||||||
public onCommand<
|
/**
|
||||||
CommandName extends s.RPCCommands_Server,
|
* Registers a chat command (FiveM `RegisterCommand`).
|
||||||
CallbackArguments extends unknown[],
|
*
|
||||||
>(
|
* @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.<name>`
|
||||||
|
* can use it
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* rpc.onCommand('heal', (player, args) => heal(player, Number(args[0])), true)
|
||||||
|
*/
|
||||||
|
public onCommand<CommandName extends RPCCommandName<s.RPCCommands_Server>>(
|
||||||
command: CommandName,
|
command: CommandName,
|
||||||
cb: (player: number, args: CallbackArguments, commandRaw: string) => void,
|
cb: (player: number, args: string[], rawCommand: string) => void,
|
||||||
restricted = false,
|
restricted = false,
|
||||||
): this {
|
): this {
|
||||||
if (this.debug) {
|
this.log(`onCommand ${command}`)
|
||||||
this.console.log(`[RPC]:onCommand ${command}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
RegisterCommand(command, cb, restricted)
|
RegisterCommand(command, cb, restricted)
|
||||||
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
public onNativeEvent<
|
/**
|
||||||
EventName extends keyof RPCNativeServerEvents,
|
* Listens to a native FiveM server event, e.g. `playerJoining`.
|
||||||
CallbackArguments extends Parameters<RPCNativeServerEvents[EventName]>,
|
*
|
||||||
>(eventName: EventName, cb: (...args: CallbackArguments) => void): this {
|
* @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 extends keyof RPCNativeServerEvents>(
|
||||||
|
eventName: EventName,
|
||||||
|
cb: (...args: Parameters<RPCNativeServerEvents[EventName]>) => void,
|
||||||
|
): this {
|
||||||
if (!NATIVE_SERVER_EVENTS.includes(eventName)) {
|
if (!NATIVE_SERVER_EVENTS.includes(eventName)) {
|
||||||
throw new Error(RPCErrors.UNKNOWN_NATIVE)
|
throw new RPCError(
|
||||||
|
RPCErrors.UNKNOWN_NATIVE,
|
||||||
|
unknownNativeMessage(eventName, 'NATIVE_SERVER_EVENTS'),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.debug) {
|
this.log(`onNativeEvent ${eventName}`)
|
||||||
this.console.log(`[RPC]:onNativeEvent ${eventName}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
on(eventName, cb)
|
on(eventName, cb)
|
||||||
|
|
||||||
|
|||||||
+172
-211
@@ -1,22 +1,34 @@
|
|||||||
import type * as s from '@entityseven/fivem-rpc-shared-types'
|
import type * as s from '@entityseven/fivem-rpc-shared-types'
|
||||||
|
|
||||||
import { Emitter } from '../utils/emitter'
|
import { Emitter } from '../utils/emitter'
|
||||||
import { generateUUID, stringify } from '../utils/funcs'
|
import { stringify } from '../utils/funcs'
|
||||||
import {
|
import {
|
||||||
RPCEvents,
|
RPCEvents,
|
||||||
type RPCConfig,
|
type RPCConfig,
|
||||||
type RPCState,
|
type RPCState,
|
||||||
type RPCStateRaw,
|
|
||||||
type RPCStateWeb,
|
type RPCStateWeb,
|
||||||
} from '../utils/types'
|
} from '../utils/types'
|
||||||
import { Wrapper } from './wrapper'
|
import type {
|
||||||
|
RPCEventArgs,
|
||||||
|
RPCEventName,
|
||||||
|
RPCEventResult,
|
||||||
|
RPCListener,
|
||||||
|
} from '../utils/typing'
|
||||||
|
import { RPCInstanceBase } from './base'
|
||||||
|
|
||||||
declare global {
|
/**
|
||||||
interface Window {
|
* RPC instance for webview code, returned by `createRPC({ env: 'webview' })`.
|
||||||
GetParentResourceName?: () => string
|
* 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*`
|
||||||
export class RPCInstanceWebview extends Wrapper {
|
* 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 _emitterClient: Emitter
|
||||||
private readonly _emitterServer: Emitter
|
private readonly _emitterServer: Emitter
|
||||||
|
|
||||||
@@ -26,75 +38,42 @@ export class RPCInstanceWebview extends Wrapper {
|
|||||||
this._emitterClient = new Emitter()
|
this._emitterClient = new Emitter()
|
||||||
this._emitterServer = new Emitter()
|
this._emitterServer = new Emitter()
|
||||||
|
|
||||||
this.console.log('[RPC] Initialized Webview')
|
console.log('[RPC] Initialized Webview')
|
||||||
|
|
||||||
window.addEventListener('message', (e: MessageEvent<RPCStateWeb>) => {
|
window.addEventListener(
|
||||||
if (e.data.origin === RPCEvents.LISTENER_CLIENT) {
|
'message',
|
||||||
this._handleClient(e.data.data)
|
(e: MessageEvent<Partial<RPCStateWeb> | null>) => {
|
||||||
|
const origin = e.data?.origin
|
||||||
|
// not ours, e.g. the resource's own SendNUIMessage calls
|
||||||
|
if (
|
||||||
|
origin !== RPCEvents.LISTENER_CLIENT &&
|
||||||
|
origin !== RPCEvents.LISTENER_SERVER
|
||||||
|
) {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
if (e.data.origin === RPCEvents.LISTENER_SERVER) {
|
|
||||||
this._handleServer(e.data.data)
|
const payload = this.accept(e.data?.data)
|
||||||
}
|
if (!payload) return
|
||||||
})
|
|
||||||
|
if (origin === RPCEvents.LISTENER_CLIENT) this._handleClient(payload)
|
||||||
|
else this._handleServer(payload)
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== HANDLERS =====
|
// ===== HANDLERS =====
|
||||||
|
|
||||||
private async _handleClient(payload: RPCState) {
|
private async _handleClient(payload: RPCState) {
|
||||||
if (this.debug) {
|
|
||||||
this.console.log(
|
|
||||||
`[RPC]:webview:accepted ${payload.type} ${payload.event} from ${payload.calledFrom}`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (payload.calledFrom === 'client' && payload.type === 'event') {
|
if (payload.calledFrom === 'client' && payload.type === 'event') {
|
||||||
this.verifyEvent(this._emitterClient, payload)
|
const response = await this.dispatch(this._emitterClient, payload)
|
||||||
|
|
||||||
const responseData = await this._emitterClient.emit(
|
await this._createHttpClientRequest(response)
|
||||||
payload.event,
|
|
||||||
...(payload.data && payload.data.length > 0 ? payload.data : []),
|
|
||||||
)
|
|
||||||
|
|
||||||
const response: RPCState = {
|
|
||||||
event: payload.event,
|
|
||||||
uuid: payload.uuid,
|
|
||||||
calledFrom: 'webview',
|
|
||||||
calledTo: 'client',
|
|
||||||
error: null,
|
|
||||||
data: [responseData],
|
|
||||||
player: payload.player,
|
|
||||||
type: 'response',
|
|
||||||
}
|
|
||||||
|
|
||||||
await this._createHttpClientRequest(response).then()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _handleServer(payload: RPCState) {
|
private async _handleServer(payload: RPCState) {
|
||||||
if (this.debug) {
|
|
||||||
this.console.log(
|
|
||||||
`[RPC]:webview:accepted ${payload.type} ${payload.event} from ${payload.calledFrom}`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (payload.calledFrom === 'server' && payload.type === 'event') {
|
if (payload.calledFrom === 'server' && payload.type === 'event') {
|
||||||
this.verifyEvent(this._emitterServer, payload)
|
const response = await this.dispatch(this._emitterServer, payload)
|
||||||
|
|
||||||
const responseData = await this._emitterServer.emit(
|
|
||||||
payload.event,
|
|
||||||
...(payload.data && payload.data.length > 0 ? payload.data : []),
|
|
||||||
)
|
|
||||||
|
|
||||||
const response: RPCState = {
|
|
||||||
event: payload.event,
|
|
||||||
uuid: payload.uuid,
|
|
||||||
calledFrom: 'webview',
|
|
||||||
calledTo: 'server',
|
|
||||||
error: null,
|
|
||||||
data: [responseData],
|
|
||||||
player: payload.player,
|
|
||||||
type: 'response',
|
|
||||||
}
|
|
||||||
|
|
||||||
await this._createHttpClientRequest(response)
|
await this._createHttpClientRequest(response)
|
||||||
}
|
}
|
||||||
@@ -102,184 +81,166 @@ export class RPCInstanceWebview extends Wrapper {
|
|||||||
|
|
||||||
// ===== CLIENT =====
|
// ===== CLIENT =====
|
||||||
|
|
||||||
public onClient<
|
/**
|
||||||
EventName extends keyof s.RPCEvents_ClientWebview,
|
* Listens for `emitWebview` calls from this player's client
|
||||||
CallbackArguments extends Parameters<s.RPCEvents_ClientWebview[EventName]>,
|
* (client -> webview).
|
||||||
CallbackReturn extends ReturnType<s.RPCEvents_ClientWebview[EventName]>,
|
*
|
||||||
|
* @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 extends RPCEventName<s.RPCEvents_ClientWebview>>(
|
||||||
|
eventName: EventName,
|
||||||
|
cb: RPCListener<s.RPCEvents_ClientWebview, EventName>,
|
||||||
|
): this {
|
||||||
|
return this.listen(this._emitterClient, 'onClient', eventName, cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Removes the `onClient` listener for `eventName` */
|
||||||
|
public offClient<EventName extends RPCEventName<s.RPCEvents_ClientWebview>>(
|
||||||
|
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<s.RPCEvents_WebviewClient>,
|
||||||
>(
|
>(
|
||||||
eventName: EventName,
|
eventName: EventName,
|
||||||
cb: (
|
...args: RPCEventArgs<s.RPCEvents_WebviewClient, EventName>
|
||||||
...args: CallbackArguments
|
): Promise<RPCEventResult<s.RPCEvents_WebviewClient, EventName>> {
|
||||||
) => Awaited<CallbackReturn> | Promise<Awaited<CallbackReturn>>,
|
const payload = this.request(eventName, 'client', args, null)
|
||||||
): this {
|
|
||||||
if (this.debug) {
|
|
||||||
this.console.log(`[RPC]:onClient ${eventName}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
this._emitterClient.on(eventName, cb)
|
return this._request(payload)
|
||||||
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
public offClient<EventName extends keyof s.RPCEvents_ClientWebview>(
|
|
||||||
eventName: EventName,
|
|
||||||
): this {
|
|
||||||
if (this.debug) {
|
|
||||||
this.console.log(`[RPC]:offClient ${eventName}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
this._emitterClient.off(eventName)
|
|
||||||
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
public async emitClient<
|
|
||||||
EventName extends keyof s.RPCEvents_WebviewClient,
|
|
||||||
Arguments extends Parameters<s.RPCEvents_WebviewClient[EventName]>,
|
|
||||||
Response extends ReturnType<s.RPCEvents_WebviewClient[EventName]>,
|
|
||||||
>(eventName: EventName, ...args: Arguments): Promise<Awaited<Response>> {
|
|
||||||
const payload: RPCState = {
|
|
||||||
event: eventName,
|
|
||||||
uuid: generateUUID(),
|
|
||||||
calledFrom: 'webview',
|
|
||||||
calledTo: 'client',
|
|
||||||
error: null,
|
|
||||||
data: args.length ? args : null,
|
|
||||||
player: null,
|
|
||||||
type: 'event',
|
|
||||||
}
|
|
||||||
|
|
||||||
return await this._createHttpClientRequest<Awaited<Response>>(payload)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== SERVER =====
|
// ===== SERVER =====
|
||||||
|
|
||||||
public onServer<
|
/**
|
||||||
EventName extends keyof s.RPCEvents_ServerWebview,
|
* Listens for `emitWebview` calls from the server (server -> webview,
|
||||||
CallbackArguments extends Parameters<s.RPCEvents_ServerWebview[EventName]>,
|
* relayed by the client).
|
||||||
CallbackReturn extends ReturnType<s.RPCEvents_ServerWebview[EventName]>,
|
*
|
||||||
|
* @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 extends RPCEventName<s.RPCEvents_ServerWebview>>(
|
||||||
|
eventName: EventName,
|
||||||
|
cb: RPCListener<s.RPCEvents_ServerWebview, EventName>,
|
||||||
|
): this {
|
||||||
|
return this.listen(this._emitterServer, 'onServer', eventName, cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Removes the `onServer` listener for `eventName` */
|
||||||
|
public offServer<EventName extends RPCEventName<s.RPCEvents_ServerWebview>>(
|
||||||
|
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<s.RPCEvents_WebviewServer>,
|
||||||
>(
|
>(
|
||||||
eventName: EventName,
|
eventName: EventName,
|
||||||
cb: (
|
...args: RPCEventArgs<s.RPCEvents_WebviewServer, EventName>
|
||||||
...args: CallbackArguments
|
): Promise<RPCEventResult<s.RPCEvents_WebviewServer, EventName>> {
|
||||||
) => Awaited<CallbackReturn> | Promise<Awaited<CallbackReturn>>,
|
const payload = this.request(eventName, 'server', args, null)
|
||||||
): this {
|
|
||||||
if (this.debug) {
|
|
||||||
this.console.log(`[RPC]:onServer ${eventName}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
this._emitterServer.on(eventName, cb)
|
return this._request(payload)
|
||||||
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
public offServer<EventName extends keyof s.RPCEvents_ServerWebview>(
|
|
||||||
eventName: EventName,
|
|
||||||
): RPCInstanceWebview {
|
|
||||||
if (this.debug) {
|
|
||||||
this.console.log(`[RPC]:offServer ${eventName}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
this._emitterServer.off(eventName)
|
|
||||||
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
public async emitServer<
|
|
||||||
EventName extends keyof s.RPCEvents_WebviewServer,
|
|
||||||
Arguments extends Parameters<s.RPCEvents_WebviewServer[EventName]>,
|
|
||||||
Response extends ReturnType<s.RPCEvents_WebviewServer[EventName]>,
|
|
||||||
>(eventName: EventName, ...args: Arguments): Promise<Awaited<Response>> {
|
|
||||||
const payload: RPCState = {
|
|
||||||
event: eventName,
|
|
||||||
uuid: generateUUID(),
|
|
||||||
calledFrom: 'webview',
|
|
||||||
calledTo: 'server',
|
|
||||||
error: null,
|
|
||||||
data: args.length ? args : null,
|
|
||||||
player: null,
|
|
||||||
type: 'event',
|
|
||||||
}
|
|
||||||
|
|
||||||
return await this._createHttpClientRequest<Awaited<Response>>(payload)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== SELF =====
|
// ===== SELF =====
|
||||||
|
|
||||||
public onSelf<
|
/**
|
||||||
EventName extends keyof s.RPCEvents_Webview,
|
* Listens for `emitSelf` calls in this environment (webview -> webview).
|
||||||
CallbackArguments extends Parameters<s.RPCEvents_Webview[EventName]>,
|
*
|
||||||
CallbackReturn extends ReturnType<s.RPCEvents_Webview[EventName]>,
|
* @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 extends RPCEventName<s.RPCEvents_Webview>>(
|
||||||
eventName: EventName,
|
eventName: EventName,
|
||||||
cb: (
|
cb: RPCListener<s.RPCEvents_Webview, EventName>,
|
||||||
...args: CallbackArguments
|
|
||||||
) => Awaited<CallbackReturn> | Promise<Awaited<CallbackReturn>>,
|
|
||||||
): this {
|
): this {
|
||||||
if (this.debug) {
|
return this.listen(this._emitterLocal, 'onSelf', eventName, cb)
|
||||||
this.console.log(`[RPC]:onSelf ${eventName}`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this._emitterLocal.on(eventName, cb)
|
/** Removes the `onSelf` listener for `eventName` */
|
||||||
|
public offSelf<EventName extends RPCEventName<s.RPCEvents_Webview>>(
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
public offSelf<EventName extends keyof s.RPCEvents_Webview>(
|
|
||||||
eventName: EventName,
|
eventName: EventName,
|
||||||
): this {
|
): this {
|
||||||
if (this.debug) {
|
return this.unlisten(this._emitterLocal, 'offSelf', eventName)
|
||||||
this.console.log(`[RPC]:offSelf ${eventName}`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this._emitterLocal.off(eventName)
|
/**
|
||||||
|
* Calls this environment's own `onSelf` listener directly and resolves with
|
||||||
return this
|
* its return value. No timeout; errors thrown by the listener reach the caller
|
||||||
}
|
* unchanged.
|
||||||
|
*
|
||||||
public async emitSelf<
|
* @throws {@link RPCError} `EVENT_NOT_REGISTERED` if no `onSelf` listener exists
|
||||||
EventName extends keyof s.RPCEvents_Webview,
|
*
|
||||||
Arguments extends Parameters<s.RPCEvents_Webview[EventName]>,
|
* @example
|
||||||
Response extends ReturnType<s.RPCEvents_Webview[EventName]>,
|
* const total = await rpc.emitSelf('add', 2, 3)
|
||||||
>(eventName: EventName, ...args: Arguments): Promise<Awaited<Response>> {
|
*/
|
||||||
const payload: RPCState = {
|
public async emitSelf<EventName extends RPCEventName<s.RPCEvents_Webview>>(
|
||||||
event: eventName,
|
eventName: EventName,
|
||||||
uuid: generateUUID(),
|
...args: RPCEventArgs<s.RPCEvents_Webview, EventName>
|
||||||
calledFrom: 'webview',
|
): Promise<RPCEventResult<s.RPCEvents_Webview, EventName>> {
|
||||||
calledTo: 'webview',
|
return this.emitLocal(eventName, args)
|
||||||
error: null,
|
|
||||||
data: args.length ? args : null,
|
|
||||||
player: null,
|
|
||||||
type: 'event',
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.debug) {
|
|
||||||
this.console.log(
|
|
||||||
`[RPC]:accepted ${payload.event} from ${payload.calledFrom}`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.verifyEvent(this._emitterLocal, payload)
|
|
||||||
|
|
||||||
return await this._emitterLocal.emit<Awaited<Response>>(
|
|
||||||
payload.event,
|
|
||||||
...(payload.data && payload.data.length > 0 ? payload.data : []),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== UTILS =====
|
// ===== UTILS =====
|
||||||
|
|
||||||
private async _createHttpClientRequest<R>(
|
/** Sends an event to the client and waits for its response (with timeout) */
|
||||||
data: RPCStateRaw | RPCState,
|
private _request<R>(payload: RPCState): Promise<R> {
|
||||||
): Promise<R> {
|
const response = this._pending.wait<R>(payload)
|
||||||
const dataRaw = typeof data === 'string' ? data : stringify(data)
|
this._createHttpClientRequest<RPCState>(payload).then(
|
||||||
|
res => {
|
||||||
|
const reply = this.accept(res)
|
||||||
|
if (reply) this.settle(reply)
|
||||||
|
},
|
||||||
|
(error: Error) => this._pending.reject(payload.uuid, error),
|
||||||
|
)
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _createHttpClientRequest<R>(data: RPCState): Promise<R> {
|
||||||
const options = {
|
const options = {
|
||||||
method: 'post',
|
method: 'post',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json; charset=UTF-8',
|
'Content-Type': 'application/json; charset=UTF-8',
|
||||||
},
|
},
|
||||||
body: dataRaw,
|
body: stringify(data),
|
||||||
}
|
}
|
||||||
|
// FiveM injects GetParentResourceName into NUI pages. Without it (page
|
||||||
|
// opened in a regular browser) the fetch fails and the call rejects.
|
||||||
const resourceName = window?.GetParentResourceName?.() ?? 'nui-frame-app'
|
const resourceName = window?.GetParentResourceName?.() ?? 'nui-frame-app'
|
||||||
return fetch(
|
return fetch(
|
||||||
`https://${resourceName}/${RPCEvents.LISTENER_WEB}`,
|
`https://${resourceName}/${RPCEvents.LISTENER_WEB}`,
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
import { Emitter } from '../utils/emitter'
|
|
||||||
import { parse } from '../utils/funcs'
|
|
||||||
import {
|
|
||||||
type RPCConfig,
|
|
||||||
type RPCEnvironment,
|
|
||||||
RPCErrors,
|
|
||||||
type RPCState,
|
|
||||||
type RPCStateRaw,
|
|
||||||
} from '../utils/types'
|
|
||||||
|
|
||||||
export class Wrapper {
|
|
||||||
protected env: RPCEnvironment
|
|
||||||
protected _emitterLocal: Emitter
|
|
||||||
protected debug: boolean
|
|
||||||
protected console: Console
|
|
||||||
|
|
||||||
constructor(cfg: RPCConfig<RPCEnvironment>) {
|
|
||||||
this.env = cfg.env
|
|
||||||
this._emitterLocal = new Emitter()
|
|
||||||
this.debug = cfg.debug ?? false
|
|
||||||
this.console = console
|
|
||||||
}
|
|
||||||
|
|
||||||
protected verifyEvent(state: Emitter, data: RPCStateRaw | RPCState) {
|
|
||||||
const rpcData = typeof data === 'string' ? parse(data) : data
|
|
||||||
|
|
||||||
if (!state.has(rpcData.event)) {
|
|
||||||
rpcData.error = RPCErrors.EVENT_NOT_REGISTERED
|
|
||||||
this.triggerError(rpcData)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected triggerError(rpcData: RPCState, error?: string): Error {
|
|
||||||
const errorMessage = [
|
|
||||||
`${rpcData.error}`,
|
|
||||||
`Event: ${rpcData.event}`,
|
|
||||||
`Uuid: ${rpcData.uuid}`,
|
|
||||||
`From: ${rpcData.calledFrom}`,
|
|
||||||
`To: ${rpcData.calledTo}`,
|
|
||||||
`Player: ${rpcData.player}`,
|
|
||||||
`Type: ${rpcData.type}`,
|
|
||||||
`Data: ${rpcData.data}`,
|
|
||||||
]
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
errorMessage.push(`Info: ${error}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(errorMessage.join('\n | '))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Vendored
+39
@@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
FiveM runtime globals used by this library. Internal: not part of the published types, so consumers are free to use @citizenfx/*.
|
||||||
|
See https://docs.fivem.net/docs/scripting-reference/runtimes/javascript/
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ===== SHARED (client + server) =====
|
||||||
|
|
||||||
|
declare function on<A extends unknown[]>(
|
||||||
|
eventName: string,
|
||||||
|
callback: (...args: A) => unknown,
|
||||||
|
): void
|
||||||
|
declare function onNet<A extends unknown[]>(
|
||||||
|
eventName: string,
|
||||||
|
callback: (...args: A) => unknown,
|
||||||
|
): void
|
||||||
|
/** Server: `emitNet(eventName, target, ...args)` */
|
||||||
|
declare function emitNet(eventName: string, ...args: unknown[]): void
|
||||||
|
declare function RegisterCommand<A extends unknown[]>(
|
||||||
|
commandName: string,
|
||||||
|
handler: (source: number, args: A, rawCommand: string) => unknown,
|
||||||
|
restricted: boolean,
|
||||||
|
): void
|
||||||
|
|
||||||
|
// ===== SERVER =====
|
||||||
|
|
||||||
|
/** Player that sent the current net event. Only valid synchronously in the handler */
|
||||||
|
declare var source: number
|
||||||
|
|
||||||
|
// ===== CLIENT =====
|
||||||
|
|
||||||
|
declare function RegisterNuiCallbackType(callbackType: string): void
|
||||||
|
declare function SetNuiFocus(hasFocus: boolean, hasCursor: boolean): void
|
||||||
|
declare function SendNuiMessage(jsonString: string): boolean
|
||||||
|
|
||||||
|
// ===== WEBVIEW (NUI) =====
|
||||||
|
|
||||||
|
interface Window {
|
||||||
|
GetParentResourceName?: () => string
|
||||||
|
}
|
||||||
+48
-53
@@ -1,7 +1,7 @@
|
|||||||
import { RPCInstanceClient } from './core/client'
|
import { RPCInstanceClient } from './core/client'
|
||||||
import { RPCInstanceServer } from './core/server'
|
import { RPCInstanceServer } from './core/server'
|
||||||
import { RPCInstanceWebview } from './core/webview'
|
import { RPCInstanceWebview } from './core/webview'
|
||||||
import { Wrapper } from './core/wrapper'
|
import { RPCError, unknownEnvironmentMessage } from './utils/errors'
|
||||||
import {
|
import {
|
||||||
type RPCConfig,
|
type RPCConfig,
|
||||||
type RPCEnvironment,
|
type RPCEnvironment,
|
||||||
@@ -10,65 +10,60 @@ import {
|
|||||||
} from './utils/types'
|
} from './utils/types'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* RPC Factory
|
* 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'`
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* // returns RPCInstanceServer
|
* // server/rpc.ts
|
||||||
* const rpc = new RPCFactory({ env: "server" }).get()
|
* import { createRPC } from '@entityseven/fivem-rpc'
|
||||||
*
|
* export const rpc = createRPC({ env: 'server' })
|
||||||
* @example
|
|
||||||
* // returns RPCInstanceClient
|
|
||||||
* const rpc = new RPCFactory({ env: "client" }).get()
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* // returns RPCInstanceWebview
|
|
||||||
* const rpc = new RPCFactory({ env: "webview" }).get()
|
|
||||||
*
|
|
||||||
* @class
|
|
||||||
*/
|
*/
|
||||||
class RPCFactory<T extends RPCEnvironment> extends Wrapper {
|
export function createRPC<T extends RPCEnvironment>(
|
||||||
private readonly operator:
|
config: RPCConfig<T>,
|
||||||
| RPCInstanceServer
|
): RPCEnvironmentResolved<T> {
|
||||||
| RPCInstanceClient
|
switch (config.env) {
|
||||||
| RPCInstanceWebview
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Instance options
|
|
||||||
* @param {object} opts - Options
|
|
||||||
* @param {string} opts.env - Instance environment
|
|
||||||
* @param {boolean} opts.debug - Show additional logs
|
|
||||||
*/
|
|
||||||
constructor(opts: RPCConfig<T>) {
|
|
||||||
super(opts)
|
|
||||||
|
|
||||||
this.console.log('[RPC] Initializing...')
|
|
||||||
|
|
||||||
switch (opts.env) {
|
|
||||||
case 'server':
|
case 'server':
|
||||||
this.operator = new RPCInstanceServer(opts as RPCConfig<'server'>)
|
return new RPCInstanceServer(
|
||||||
break
|
config as RPCConfig<'server'>,
|
||||||
|
) as RPCEnvironmentResolved<T>
|
||||||
case 'client':
|
case 'client':
|
||||||
this.operator = new RPCInstanceClient(opts as RPCConfig<'client'>)
|
return new RPCInstanceClient(
|
||||||
break
|
config as RPCConfig<'client'>,
|
||||||
|
) as RPCEnvironmentResolved<T>
|
||||||
case 'webview':
|
case 'webview':
|
||||||
this.operator = new RPCInstanceWebview(opts as RPCConfig<'webview'>)
|
return new RPCInstanceWebview(
|
||||||
break
|
config as RPCConfig<'webview'>,
|
||||||
|
) as RPCEnvironmentResolved<T>
|
||||||
default:
|
default:
|
||||||
throw new Error(RPCErrors.UNKNOWN_ENVIRONMENT)
|
throw new RPCError(
|
||||||
|
RPCErrors.UNKNOWN_ENVIRONMENT,
|
||||||
|
unknownEnvironmentMessage(config.env),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public get(): RPCEnvironmentResolved<T> {
|
export type { RPCInstanceClient } from './core/client'
|
||||||
return this.operator as RPCEnvironmentResolved<T>
|
export type { RPCInstanceServer } from './core/server'
|
||||||
}
|
export type { RPCInstanceWebview } from './core/webview'
|
||||||
}
|
export { RPCError, type RPCErrorDetails } from './utils/errors'
|
||||||
|
export {
|
||||||
export { RPCFactory }
|
NATIVE_CLIENT_EVENTS,
|
||||||
// export const rpcClient = new RPCFactory({ env: "client" }).get();
|
NATIVE_CLIENT_NETWORK_EVENTS,
|
||||||
// export const rpcServer = new RPCFactory({ env: "server" }).get();
|
NATIVE_SERVER_EVENTS,
|
||||||
// export const rpcWebview = new RPCFactory({ env: "webview" }).get();
|
} from './utils/native'
|
||||||
export * from './utils/types'
|
export {
|
||||||
export * from './utils/native'
|
type RPCConfig,
|
||||||
export type * from './core/server'
|
type RPCEnvironment,
|
||||||
export type * from './core/client'
|
RPCErrors,
|
||||||
export type * from './core/webview'
|
type RPCNativeClientEvents,
|
||||||
|
type RPCNativeClientNetworkEvents,
|
||||||
|
type RPCNativeClientNetworkEventsNames,
|
||||||
|
type RPCNativeServerEvents,
|
||||||
|
} from './utils/types'
|
||||||
|
|||||||
+17
-25
@@ -1,23 +1,22 @@
|
|||||||
import { RPCErrors } from './types'
|
import { RPCErrors } from './types'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Accepts any callback. Argument types are enforced by the typed `on*`/`emit*`
|
||||||
|
* methods that wrap the emitter, not here.
|
||||||
|
*/
|
||||||
|
export type Handler = (...args: never[]) => unknown
|
||||||
|
|
||||||
|
/** One handler per event: registering an event again replaces its handler. */
|
||||||
export class Emitter {
|
export class Emitter {
|
||||||
/** Map<event, [callback function, once]> */
|
/** Map<event, [handler, once]> */
|
||||||
private _storage: Map<string, [(...args: any[]) => any, boolean]>
|
private _storage = new Map<string, [Handler, boolean]>()
|
||||||
|
|
||||||
constructor() {
|
public on(event: string, cb: Handler): this {
|
||||||
this._storage = new Map()
|
|
||||||
}
|
|
||||||
|
|
||||||
get _raw_storage() {
|
|
||||||
return this._storage
|
|
||||||
}
|
|
||||||
|
|
||||||
public on(event: string, cb: (...args: any[]) => any): this {
|
|
||||||
this._storage.set(event, [cb, false])
|
this._storage.set(event, [cb, false])
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
public once(event: string, cb: (...args: any[]) => any): this {
|
public once(event: string, cb: Handler): this {
|
||||||
this._storage.set(event, [cb, true])
|
this._storage.set(event, [cb, true])
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
@@ -31,24 +30,17 @@ export class Emitter {
|
|||||||
return this._storage.has(event)
|
return this._storage.has(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
public async emit<R>(event: string, ...args: any[]): Promise<R> {
|
public async emit<R>(event: string, ...args: unknown[]): Promise<R> {
|
||||||
return new Promise((res, rej) => {
|
const entry = this._storage.get(event)
|
||||||
if (!this._storage.has(event)) {
|
if (!entry) {
|
||||||
rej(RPCErrors.EVENT_NOT_REGISTERED)
|
throw new Error(RPCErrors.EVENT_NOT_REGISTERED)
|
||||||
}
|
}
|
||||||
|
|
||||||
const [cb, once] = this._storage.get(event) as [
|
const [cb, once] = entry
|
||||||
(...args: any[]) => any,
|
|
||||||
boolean,
|
|
||||||
]
|
|
||||||
|
|
||||||
if (once) {
|
if (once) {
|
||||||
this._storage.delete(event)
|
this._storage.delete(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
Promise.resolve(cb(...args))
|
return (await cb(...(args as never[]))) as R
|
||||||
.then(res)
|
|
||||||
.catch(rej)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import { type RPCEnvironment, RPCErrors, type RPCState } from './types'
|
||||||
|
|
||||||
|
/** Where a failed call was going, when the error comes from a call */
|
||||||
|
export type RPCErrorDetails = {
|
||||||
|
event: string
|
||||||
|
uuid: string
|
||||||
|
/** Environment that made the call */
|
||||||
|
from: RPCEnvironment
|
||||||
|
/** Environment that was called */
|
||||||
|
to: RPCEnvironment
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error thrown or rejected by this library. Check `code` against `RPCErrors`.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* try {
|
||||||
|
* await rpc.emitServer('buyItem', 'water')
|
||||||
|
* } catch (e) {
|
||||||
|
* if (e instanceof RPCError && e.code === RPCErrors.TIMEOUT) {
|
||||||
|
* // server did not answer in time
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
export class RPCError extends Error {
|
||||||
|
public readonly code: RPCErrors
|
||||||
|
public readonly details: RPCErrorDetails | undefined
|
||||||
|
|
||||||
|
constructor(code: RPCErrors, message: string, details?: RPCErrorDetails) {
|
||||||
|
super(message)
|
||||||
|
this.name = 'RPCError'
|
||||||
|
this.code = code
|
||||||
|
this.details = details
|
||||||
|
}
|
||||||
|
|
||||||
|
/** **Internal** Rebuilds the error a receiver sent back in a response */
|
||||||
|
static fromResponse(response: RPCState): RPCError {
|
||||||
|
return new RPCError(
|
||||||
|
response.error?.code ?? RPCErrors.HANDLER_ERROR,
|
||||||
|
response.error?.message ?? 'Unknown error',
|
||||||
|
{
|
||||||
|
event: response.event,
|
||||||
|
uuid: response.uuid,
|
||||||
|
from: response.calledTo,
|
||||||
|
to: response.calledFrom,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** **Internal** Name of the method that listens on `receiver` for calls from `sender` */
|
||||||
|
export function listenerName(
|
||||||
|
receiver: RPCEnvironment,
|
||||||
|
sender: RPCEnvironment,
|
||||||
|
): string {
|
||||||
|
if (receiver === sender) return 'onSelf'
|
||||||
|
return `on${sender[0]?.toUpperCase()}${sender.slice(1)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/** **Internal** */
|
||||||
|
export function notRegisteredMessage(
|
||||||
|
event: string,
|
||||||
|
receiver: RPCEnvironment,
|
||||||
|
sender: RPCEnvironment,
|
||||||
|
): string {
|
||||||
|
return `No listener for "${event}" on ${receiver}. Register it with rpc.${listenerName(receiver, sender)}("${event}", ...) in ${receiver} code.`
|
||||||
|
}
|
||||||
|
|
||||||
|
/** **Internal** */
|
||||||
|
export function unknownEnvironmentMessage(env: unknown): string {
|
||||||
|
return `Unknown env ${JSON.stringify(env)}. Use createRPC({ env: 'server' }), 'client' or 'webview'.`
|
||||||
|
}
|
||||||
|
|
||||||
|
/** **Internal** */
|
||||||
|
export function unknownNativeMessage(event: string, list: string): string {
|
||||||
|
return `"${event}" is not in ${list}. Events not listed there can be registered with FiveM's on("${event}", ...) directly.`
|
||||||
|
}
|
||||||
|
|
||||||
|
/** **Internal** */
|
||||||
|
export function handlerErrorMessage(
|
||||||
|
event: string,
|
||||||
|
receiver: RPCEnvironment,
|
||||||
|
cause: unknown,
|
||||||
|
): string {
|
||||||
|
const reason =
|
||||||
|
cause instanceof Error || (cause as Error | undefined)?.message
|
||||||
|
? (cause as Error).message
|
||||||
|
: String(cause)
|
||||||
|
return `Listener for "${event}" on ${receiver} threw: ${reason}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/** **Internal** */
|
||||||
|
export function timeoutMessage(
|
||||||
|
event: string,
|
||||||
|
receiver: RPCEnvironment,
|
||||||
|
sender: RPCEnvironment,
|
||||||
|
timeout: number,
|
||||||
|
): string {
|
||||||
|
return `No response for "${event}" from ${receiver} within ${timeout} ms. Check that ${receiver} registered rpc.${listenerName(receiver, sender)}("${event}", ...) and that it returns, or raise RPCConfig.timeout (0 disables it).`
|
||||||
|
}
|
||||||
+44
-10
@@ -1,17 +1,48 @@
|
|||||||
import type {
|
import type {
|
||||||
|
RPCEnvironment,
|
||||||
RPCState,
|
RPCState,
|
||||||
RPCStateRaw,
|
RPCStateRaw,
|
||||||
RPCStateWeb,
|
RPCStateWeb,
|
||||||
RPCStateWebRaw,
|
RPCStateWebRaw,
|
||||||
} from './types'
|
} from './types'
|
||||||
|
|
||||||
|
const ENVIRONMENTS: readonly unknown[] = [
|
||||||
|
'server',
|
||||||
|
'client',
|
||||||
|
'webview',
|
||||||
|
] satisfies RPCEnvironment[]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* **Internal**
|
* **Internal**
|
||||||
*
|
*
|
||||||
* Typed data parser
|
* Checks the shape of an incoming payload. Payloads come from the network or
|
||||||
|
* another runtime, so nothing about them is trusted.
|
||||||
*/
|
*/
|
||||||
export function parse(data: RPCStateRaw): RPCState {
|
export function isRPCState(value: unknown): value is RPCState {
|
||||||
return JSON.parse(data)
|
if (typeof value !== 'object' || value === null) return false
|
||||||
|
const v = value as Record<string, unknown>
|
||||||
|
return (
|
||||||
|
typeof v.event === 'string' &&
|
||||||
|
typeof v.uuid === 'string' &&
|
||||||
|
(v.type === 'event' || v.type === 'response' || v.type === 'broadcast') &&
|
||||||
|
ENVIRONMENTS.includes(v.calledFrom) &&
|
||||||
|
ENVIRONMENTS.includes(v.calledTo) &&
|
||||||
|
(v.data === null || Array.isArray(v.data))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* **Internal**
|
||||||
|
*
|
||||||
|
* Parses a raw payload, `null` if it is not JSON or not an RPC payload
|
||||||
|
*/
|
||||||
|
export function parse(data: RPCStateRaw): RPCState | null {
|
||||||
|
try {
|
||||||
|
const value: unknown = JSON.parse(data)
|
||||||
|
return isRPCState(value) ? value : null
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -23,21 +54,24 @@ export function stringify(data: RPCState): RPCStateRaw {
|
|||||||
return JSON.stringify(data) as RPCStateRaw
|
return JSON.stringify(data) as RPCStateRaw
|
||||||
}
|
}
|
||||||
|
|
||||||
// automatically parsed by FiveM
|
|
||||||
// export function parseWeb(data: RPCStateWebRaw): RPCStateWeb {
|
|
||||||
// return JSON.parse(data)
|
|
||||||
// }
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* **Internal**
|
* **Internal**
|
||||||
*
|
*
|
||||||
* Typed data serializer
|
* Typed data serializer. No parse counterpart: the webview receives NUI
|
||||||
|
* messages already parsed by FiveM.
|
||||||
*/
|
*/
|
||||||
export function stringifyWeb(data: RPCStateWeb): RPCStateWebRaw {
|
export function stringifyWeb(data: RPCStateWeb): RPCStateWebRaw {
|
||||||
return JSON.stringify(data) as RPCStateWebRaw
|
return JSON.stringify(data) as RPCStateWebRaw
|
||||||
}
|
}
|
||||||
|
|
||||||
/** **Internal** */
|
/**
|
||||||
|
* **Internal**
|
||||||
|
*
|
||||||
|
* UUID v4 shaped call id. Not `crypto.randomUUID`: the FiveM client runtime
|
||||||
|
* has no `crypto` global, and one generator serves all environments. Only
|
||||||
|
* pairs a response with its call, so it need not be unguessable: the server
|
||||||
|
* binds each call to its target player (`Pending` peer check).
|
||||||
|
*/
|
||||||
export function generateUUID(): string {
|
export function generateUUID(): string {
|
||||||
let uuid = ''
|
let uuid = ''
|
||||||
let random = 0
|
let random = 0
|
||||||
|
|||||||
+49
-41
@@ -1,56 +1,64 @@
|
|||||||
import type {
|
import type { RPCNativeClientEvents, RPCNativeServerEvents } from './types'
|
||||||
RPCNativeClientEvents,
|
|
||||||
RPCNativeClientNetworkEventsNames,
|
// Server and client lists are built from an object that must name exactly the
|
||||||
RPCNativeServerEvents,
|
// keys of the typed event map, so the list and the types cannot drift apart.
|
||||||
} from './types'
|
|
||||||
|
const serverEvents = {
|
||||||
|
entityCreated: true,
|
||||||
|
entityCreating: true,
|
||||||
|
entityRemoved: true,
|
||||||
|
onResourceListRefresh: true,
|
||||||
|
onResourceStart: true,
|
||||||
|
onResourceStarting: true,
|
||||||
|
onResourceStop: true,
|
||||||
|
onServerResourceStart: true,
|
||||||
|
onServerResourceStop: true,
|
||||||
|
playerConnecting: true,
|
||||||
|
playerEnteredScope: true,
|
||||||
|
playerJoining: true,
|
||||||
|
playerLeftScope: true,
|
||||||
|
ptFxEvent: true,
|
||||||
|
removeAllWeaponsEvent: true,
|
||||||
|
startProjectileEvent: true,
|
||||||
|
weaponDamageEvent: true,
|
||||||
|
} satisfies Record<keyof RPCNativeServerEvents, true>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Events `onNativeEvent` accepts on the server:
|
||||||
* https://docs.fivem.net/docs/scripting-reference/events/server-events/
|
* https://docs.fivem.net/docs/scripting-reference/events/server-events/
|
||||||
* @readonly
|
|
||||||
*/
|
*/
|
||||||
export const NATIVE_SERVER_EVENTS: readonly (keyof RPCNativeServerEvents)[] = [
|
export const NATIVE_SERVER_EVENTS = Object.keys(
|
||||||
'entityCreated',
|
serverEvents,
|
||||||
'entityCreating',
|
) as readonly (keyof RPCNativeServerEvents)[]
|
||||||
'entityRemoved',
|
|
||||||
'onResourceListRefresh',
|
const clientEvents = {
|
||||||
'onResourceStart',
|
entityDamaged: true,
|
||||||
'onResourceStarting',
|
gameEventTriggered: true,
|
||||||
'onResourceStop',
|
mumbleConnected: true,
|
||||||
'onServerResourceStart',
|
mumbleDisconnected: true,
|
||||||
'onServerResourceStop',
|
onClientResourceStart: true,
|
||||||
'playerConnecting',
|
onClientResourceStop: true,
|
||||||
'playerEnteredScope',
|
onResourceStart: true,
|
||||||
'playerJoining',
|
onResourceStarting: true,
|
||||||
'playerLeftScope',
|
onResourceStop: true,
|
||||||
'ptFxEvent',
|
populationPedCreating: true,
|
||||||
'removeAllWeaponsEvent',
|
} satisfies Record<keyof RPCNativeClientEvents, true>
|
||||||
'startProjectileEvent',
|
|
||||||
'weaponDamageEvent',
|
|
||||||
] as const
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Events `onNativeEvent` accepts on the client:
|
||||||
* https://docs.fivem.net/docs/scripting-reference/events/client-events/
|
* https://docs.fivem.net/docs/scripting-reference/events/client-events/
|
||||||
* @readonly
|
|
||||||
*/
|
*/
|
||||||
export const NATIVE_CLIENT_EVENTS: readonly (keyof RPCNativeClientEvents)[] = [
|
export const NATIVE_CLIENT_EVENTS = Object.keys(
|
||||||
'entityDamaged',
|
clientEvents,
|
||||||
'gameEventTriggered',
|
) as readonly (keyof RPCNativeClientEvents)[]
|
||||||
'mumbleConnected',
|
|
||||||
'mumbleDisconnected',
|
|
||||||
'onClientResourceStart',
|
|
||||||
'onClientResourceStop',
|
|
||||||
'onResourceStart',
|
|
||||||
'onResourceStarting',
|
|
||||||
'onResourceStop',
|
|
||||||
'populationPedCreating',
|
|
||||||
] as const
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Events `onNativeNetworkEvent` accepts on the client:
|
||||||
* https://docs.fivem.net/docs/game-references/game-events/
|
* https://docs.fivem.net/docs/game-references/game-events/
|
||||||
* @readonly
|
*
|
||||||
|
* Source of truth for `RPCNativeClientNetworkEventsNames`.
|
||||||
*/
|
*/
|
||||||
export const NATIVE_CLIENT_NETWORK_EVENTS: readonly RPCNativeClientNetworkEventsNames[] =
|
export const NATIVE_CLIENT_NETWORK_EVENTS = [
|
||||||
[
|
|
||||||
'CEventAcquaintancePed',
|
'CEventAcquaintancePed',
|
||||||
'CEventAcquaintancePedDead',
|
'CEventAcquaintancePedDead',
|
||||||
'CEventAcquaintancePedDislike',
|
'CEventAcquaintancePedDislike',
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import { RPCError, timeoutMessage } from './errors'
|
||||||
|
import { RPCErrors, type RPCState } from './types'
|
||||||
|
|
||||||
|
type Call = {
|
||||||
|
resolve: (data: unknown) => void
|
||||||
|
reject: (error: Error) => void
|
||||||
|
timer: ReturnType<typeof setTimeout> | undefined
|
||||||
|
/** Only a response from this peer settles the call (server: target player) */
|
||||||
|
peer: number | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Calls waiting for a response, keyed by payload uuid. */
|
||||||
|
export class Pending {
|
||||||
|
private _calls = new Map<string, Call>()
|
||||||
|
|
||||||
|
/** @param timeout - ms before a call rejects, `0` or less disables it */
|
||||||
|
constructor(private readonly _timeout: number) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Waits for the response to `request`, rejects with `RPCErrors.TIMEOUT`.
|
||||||
|
*
|
||||||
|
* @param peer - if set, only a response from this peer settles the call
|
||||||
|
*/
|
||||||
|
public wait<R>(request: RPCState, peer?: number): Promise<R> {
|
||||||
|
return new Promise<R>((resolve, reject) => {
|
||||||
|
const timer =
|
||||||
|
this._timeout > 0
|
||||||
|
? setTimeout(
|
||||||
|
() =>
|
||||||
|
this.reject(request.uuid, this._timeoutError(request), peer),
|
||||||
|
this._timeout,
|
||||||
|
)
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
this._calls.set(request.uuid, {
|
||||||
|
resolve: resolve as (data: unknown) => void,
|
||||||
|
reject,
|
||||||
|
timer,
|
||||||
|
peer,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @returns `false` if no call from `peer` waits for this uuid (late or unexpected response) */
|
||||||
|
public resolve(uuid: string, data: unknown, peer?: number): boolean {
|
||||||
|
const call = this._take(uuid, peer)
|
||||||
|
call?.resolve(data)
|
||||||
|
return call !== undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @returns `false` if no call from `peer` waits for this uuid */
|
||||||
|
public reject(uuid: string, error: Error, peer?: number): boolean {
|
||||||
|
const call = this._take(uuid, peer)
|
||||||
|
call?.reject(error)
|
||||||
|
return call !== undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
private _take(uuid: string, peer: number | undefined): Call | undefined {
|
||||||
|
const call = this._calls.get(uuid)
|
||||||
|
if (!call || (call.peer !== undefined && call.peer !== peer)) return
|
||||||
|
|
||||||
|
this._calls.delete(uuid)
|
||||||
|
clearTimeout(call.timer)
|
||||||
|
return call
|
||||||
|
}
|
||||||
|
|
||||||
|
private _timeoutError(request: RPCState): RPCError {
|
||||||
|
return new RPCError(
|
||||||
|
RPCErrors.TIMEOUT,
|
||||||
|
timeoutMessage(
|
||||||
|
request.event,
|
||||||
|
request.calledTo,
|
||||||
|
request.calledFrom,
|
||||||
|
this._timeout,
|
||||||
|
),
|
||||||
|
{
|
||||||
|
event: request.event,
|
||||||
|
uuid: request.uuid,
|
||||||
|
from: request.calledFrom,
|
||||||
|
to: request.calledTo,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+51
-286
@@ -1,9 +1,11 @@
|
|||||||
import type { RPCInstanceClient } from '../core/client'
|
import type { RPCInstanceClient } from '../core/client'
|
||||||
import type { RPCInstanceServer } from '../core/server'
|
import type { RPCInstanceServer } from '../core/server'
|
||||||
import type { RPCInstanceWebview } from '../core/webview'
|
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'
|
export type RPCEnvironment = 'server' | 'client' | 'webview'
|
||||||
|
|
||||||
@@ -16,18 +18,33 @@ export type RPCEnvironmentResolved<T extends RPCEnvironment> =
|
|||||||
? RPCInstanceWebview
|
? RPCInstanceWebview
|
||||||
: never
|
: never
|
||||||
|
|
||||||
/**
|
/** `createRPC` config */
|
||||||
* `RPCFactory` config.
|
export type RPCConfig<T extends RPCEnvironment = RPCEnvironment> = {
|
||||||
*
|
/** Environment this instance runs in */
|
||||||
* If environment does not match will throw `RPCErrors.UNKNOWN_ENVIRONMENT`
|
|
||||||
*/
|
|
||||||
export type RPCConfig<T extends RPCEnvironment | unknown> = {
|
|
||||||
env: T
|
env: T
|
||||||
|
/**
|
||||||
|
* Log every registration, call and incoming payload
|
||||||
|
*
|
||||||
|
* @defaultValue false
|
||||||
|
*/
|
||||||
debug?: boolean
|
debug?: boolean
|
||||||
|
/**
|
||||||
|
* Milliseconds to wait for a response before the call rejects with
|
||||||
|
* `RPCErrors.TIMEOUT`. `0` disables the timeout.
|
||||||
|
*
|
||||||
|
* @defaultValue 5000
|
||||||
|
*/
|
||||||
|
timeout?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
/** **Internal** */
|
/**
|
||||||
export type RPCEventType = 'event' | 'response'
|
* **Internal**
|
||||||
|
*
|
||||||
|
* - `event`: call that expects a `response`
|
||||||
|
* - `response`: answer to an `event`
|
||||||
|
* - `broadcast`: one-way event, receivers do not reply
|
||||||
|
*/
|
||||||
|
export type RPCEventType = 'event' | 'response' | 'broadcast'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* **Internal**
|
* **Internal**
|
||||||
@@ -39,12 +56,19 @@ export type RPCState = {
|
|||||||
uuid: string
|
uuid: string
|
||||||
calledFrom: RPCEnvironment
|
calledFrom: RPCEnvironment
|
||||||
calledTo: RPCEnvironment
|
calledTo: RPCEnvironment
|
||||||
error: string | null
|
error: RPCErrorPayload | null
|
||||||
data: unknown[] | null
|
data: unknown[] | null
|
||||||
|
/** Server id of the player involved. The server fills it from `source`, never trusting the sender */
|
||||||
player: number | null
|
player: number | null
|
||||||
type: RPCEventType
|
type: RPCEventType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** **Internal** Error sent back in a response, rebuilt as `RPCError` by the caller */
|
||||||
|
export type RPCErrorPayload = {
|
||||||
|
code: RPCErrors
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* **Internal**
|
* **Internal**
|
||||||
*
|
*
|
||||||
@@ -76,18 +100,22 @@ export enum RPCEvents {
|
|||||||
LISTENER_WEB = '__rpc:listenerWeb',
|
LISTENER_WEB = '__rpc:listenerWeb',
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Values of `RPCError.code` */
|
||||||
* Errors to check against
|
|
||||||
*/
|
|
||||||
export enum RPCErrors {
|
export enum RPCErrors {
|
||||||
|
/** The target has no listener for the event (or `emitSelf` has no `onSelf`) */
|
||||||
EVENT_NOT_REGISTERED = 'Event not registered',
|
EVENT_NOT_REGISTERED = 'Event not registered',
|
||||||
INVALID_DATA = 'Invalid data (possibly broken JSON)',
|
/** `onNative*` got an event that is not in its `NATIVE_*` list */
|
||||||
NO_PLAYER = 'No player (failed to resolve from local index)',
|
UNKNOWN_NATIVE = 'Unknown native event',
|
||||||
UNKNOWN_NATIVE = 'Unknown native event (if you are sure this exists - use native handler)',
|
/** `createRPC` got an `env` other than server, client or webview */
|
||||||
UNKNOWN_ENVIRONMENT = 'Unknown environment (must be either "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/
|
* https://docs.fivem.net/docs/scripting-reference/events/server-events/
|
||||||
*/
|
*/
|
||||||
export type RPCNativeServerEvents = {
|
export type RPCNativeServerEvents = {
|
||||||
@@ -222,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/
|
* https://docs.fivem.net/docs/scripting-reference/events/client-events/
|
||||||
*/
|
*/
|
||||||
export type RPCNativeClientEvents = {
|
export type RPCNativeClientEvents = {
|
||||||
@@ -232,7 +261,7 @@ export type RPCNativeClientEvents = {
|
|||||||
baseDamage: number,
|
baseDamage: number,
|
||||||
): void
|
): void
|
||||||
gameEventTriggered(
|
gameEventTriggered(
|
||||||
name: RPCNativeClientNetworksEvents | string,
|
name: RPCNativeClientNetworkEventsNames | (string & {}),
|
||||||
data: number[],
|
data: number[],
|
||||||
): void
|
): void
|
||||||
mumbleConnected(address: string, reconnecting: boolean): void
|
mumbleConnected(address: string, reconnecting: boolean): void
|
||||||
@@ -254,7 +283,8 @@ export type RPCNativeClientEvents = {
|
|||||||
): void
|
): void
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RPCNativeClientNetworksEvents = {
|
/** Game events accepted by `onNativeNetworkEvent` on the client */
|
||||||
|
export type RPCNativeClientNetworkEvents = {
|
||||||
[name in RPCNativeClientNetworkEventsNames]: (
|
[name in RPCNativeClientNetworkEventsNames]: (
|
||||||
entities: number[],
|
entities: number[],
|
||||||
eventEntity: number,
|
eventEntity: number,
|
||||||
@@ -263,273 +293,8 @@ export type RPCNativeClientNetworksEvents = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Names in `NATIVE_CLIENT_NETWORK_EVENTS`:
|
||||||
* https://docs.fivem.net/docs/game-references/game-events/
|
* https://docs.fivem.net/docs/game-references/game-events/
|
||||||
*/
|
*/
|
||||||
export type RPCNativeClientNetworkEventsNames =
|
export type RPCNativeClientNetworkEventsNames =
|
||||||
| 'CEventAcquaintancePed'
|
(typeof NATIVE_CLIENT_NETWORK_EVENTS)[number]
|
||||||
| 'CEventAcquaintancePedDead'
|
|
||||||
| 'CEventAcquaintancePedDislike'
|
|
||||||
| 'CEventAcquaintancePedHate'
|
|
||||||
| 'CEventAcquaintancePedLike'
|
|
||||||
| 'CEventAcquaintancePedWanted'
|
|
||||||
| 'CEventAgitated'
|
|
||||||
| 'CEventAgitatedAction'
|
|
||||||
| 'CEventCallForCover'
|
|
||||||
| 'CEventCarUndriveable'
|
|
||||||
| 'CEventClimbLadderOnRoute'
|
|
||||||
| 'CEventClimbNavMeshOnRoute'
|
|
||||||
| 'CEventCombatTaunt'
|
|
||||||
| 'CEventCommunicateEvent'
|
|
||||||
| 'CEventCopCarBeingStolen'
|
|
||||||
| 'CEventCrimeCryForHelp'
|
|
||||||
| 'CEventCrimeReported'
|
|
||||||
| 'CEventDamage'
|
|
||||||
| 'CEventDataDecisionMaker'
|
|
||||||
| 'CEventDataFileMounter'
|
|
||||||
| 'CEventDataResponseAggressiveRubberneck'
|
|
||||||
| 'CEventDataResponseDeferToScenarioPointFlags'
|
|
||||||
| 'CEventDataResponseFriendlyAimedAt'
|
|
||||||
| 'CEventDataResponseFriendlyNearMiss'
|
|
||||||
| 'CEventDataResponsePlayerDeath'
|
|
||||||
| 'CEventDataResponsePoliceTaskWanted'
|
|
||||||
| 'CEventDataResponseSwatTaskWanted'
|
|
||||||
| 'CEventDataResponseTask'
|
|
||||||
| 'CEventDataResponseTaskAgitated'
|
|
||||||
| 'CEventDataResponseTaskCombat'
|
|
||||||
| 'CEventDataResponseTaskCower'
|
|
||||||
| 'CEventDataResponseTaskCrouch'
|
|
||||||
| 'CEventDataResponseTaskDuckAndCover'
|
|
||||||
| 'CEventDataResponseTaskEscapeBlast'
|
|
||||||
| 'CEventDataResponseTaskEvasiveStep'
|
|
||||||
| 'CEventDataResponseTaskExhaustedFlee'
|
|
||||||
| 'CEventDataResponseTaskExplosion'
|
|
||||||
| 'CEventDataResponseTaskFlee'
|
|
||||||
| 'CEventDataResponseTaskFlyAway'
|
|
||||||
| 'CEventDataResponseTaskGrowlAndFlee'
|
|
||||||
| 'CEventDataResponseTaskGunAimedAt'
|
|
||||||
| 'CEventDataResponseTaskHandsUp'
|
|
||||||
| 'CEventDataResponseTaskHeadTrack'
|
|
||||||
| 'CEventDataResponseTaskLeaveCarAndFlee'
|
|
||||||
| 'CEventDataResponseTaskScenarioFlee'
|
|
||||||
| 'CEventDataResponseTaskSharkAttack'
|
|
||||||
| 'CEventDataResponseTaskShockingEventBackAway'
|
|
||||||
| 'CEventDataResponseTaskShockingEventGoto'
|
|
||||||
| 'CEventDataResponseTaskShockingEventHurryAway'
|
|
||||||
| 'CEventDataResponseTaskShockingEventReact'
|
|
||||||
| 'CEventDataResponseTaskShockingEventReactToAircraft'
|
|
||||||
| 'CEventDataResponseTaskShockingEventStopAndStare'
|
|
||||||
| 'CEventDataResponseTaskShockingEventThreatResponse'
|
|
||||||
| 'CEventDataResponseTaskShockingEventWatch'
|
|
||||||
| 'CEventDataResponseTaskShockingNiceCar'
|
|
||||||
| 'CEventDataResponseTaskShockingPoliceInvestigate'
|
|
||||||
| 'CEventDataResponseTaskThreat'
|
|
||||||
| 'CEventDataResponseTaskTurnToFace'
|
|
||||||
| 'CEventDataResponseTaskWalkAway'
|
|
||||||
| 'CEventDataResponseTaskWalkRoundEntity'
|
|
||||||
| 'CEventDataResponseTaskWalkRoundFire'
|
|
||||||
| 'CEventDeadPedFound'
|
|
||||||
| 'CEventDeath'
|
|
||||||
| 'CEventDecisionMakerResponse'
|
|
||||||
| 'CEventDisturbance'
|
|
||||||
| 'CEventDraggedOutCar'
|
|
||||||
| 'CEventEditableResponse'
|
|
||||||
| 'CEventEncroachingPed'
|
|
||||||
| 'CEventEntityDamaged'
|
|
||||||
| 'CEventEntityDestroyed'
|
|
||||||
| 'CEventExplosion'
|
|
||||||
| 'CEventExplosionHeard'
|
|
||||||
| 'CEventFireNearby'
|
|
||||||
| 'CEventFootStepHeard'
|
|
||||||
| 'CEventFriendlyAimedAt'
|
|
||||||
| 'CEventFriendlyFireNearMiss'
|
|
||||||
| 'CEventGetOutOfWater'
|
|
||||||
| 'CEventGivePedTask'
|
|
||||||
| 'CEventGroupScriptAI'
|
|
||||||
| 'CEventGroupScriptNetwork'
|
|
||||||
| 'CEventGunAimedAt'
|
|
||||||
| 'CEventGunShot'
|
|
||||||
| 'CEventGunShotBulletImpact'
|
|
||||||
| 'CEventGunShotWhizzedBy'
|
|
||||||
| 'CEventHelpAmbientFriend'
|
|
||||||
| 'CEventHurtTransition'
|
|
||||||
| 'CEventInAir'
|
|
||||||
| 'CEventInfo'
|
|
||||||
| 'CEventInfoBase'
|
|
||||||
| 'CEventInjuredCryForHelp'
|
|
||||||
| 'CEventLeaderEnteredCarAsDriver'
|
|
||||||
| 'CEventLeaderExitedCarAsDriver'
|
|
||||||
| 'CEventLeaderHolsteredWeapon'
|
|
||||||
| 'CEventLeaderLeftCover'
|
|
||||||
| 'CEventLeaderUnholsteredWeapon'
|
|
||||||
| 'CEventMeleeAction'
|
|
||||||
| 'CEventMustLeaveBoat'
|
|
||||||
| 'CEventNetworkAdminInvited'
|
|
||||||
| 'CEventNetworkAttemptHostMigration'
|
|
||||||
| 'CEventNetworkBail'
|
|
||||||
| 'CEventNetworkCashTransactionLog'
|
|
||||||
| 'CEventNetworkCheatTriggered'
|
|
||||||
| 'CEventNetworkClanInviteReceived'
|
|
||||||
| 'CEventNetworkClanJoined'
|
|
||||||
| 'CEventNetworkClanKicked'
|
|
||||||
| 'CEventNetworkClanLeft'
|
|
||||||
| 'CEventNetworkClanRankChanged'
|
|
||||||
| 'CEventNetworkCloudEvent'
|
|
||||||
| 'CEventNetworkCloudFileResponse'
|
|
||||||
| 'CEventNetworkEmailReceivedEvent'
|
|
||||||
| 'CEventNetworkEndMatch'
|
|
||||||
| 'CEventNetworkEndSession'
|
|
||||||
| 'CEventNetworkEntityDamage'
|
|
||||||
| 'CEventNetworkFindSession'
|
|
||||||
| 'CEventNetworkFollowInviteReceived'
|
|
||||||
| 'CEventNetworkHostMigration'
|
|
||||||
| 'CEventNetworkHostSession'
|
|
||||||
| 'CEventNetworkIncrementStat'
|
|
||||||
| 'CEventNetworkInviteAccepted'
|
|
||||||
| 'CEventNetworkInviteConfirmed'
|
|
||||||
| 'CEventNetworkInviteRejected'
|
|
||||||
| 'CEventNetworkJoinSession'
|
|
||||||
| 'CEventNetworkJoinSessionResponse'
|
|
||||||
| 'CEventNetworkOnlinePermissionsUpdated'
|
|
||||||
| 'CEventNetworkPedLeftBehind'
|
|
||||||
| 'CEventNetworkPickupRespawned'
|
|
||||||
| 'CEventNetworkPlayerArrest'
|
|
||||||
| 'CEventNetworkPlayerCollectedAmbientPickup'
|
|
||||||
| 'CEventNetworkPlayerCollectedPickup'
|
|
||||||
| 'CEventNetworkPlayerCollectedPortablePickup'
|
|
||||||
| 'CEventNetworkPlayerDroppedPortablePickup'
|
|
||||||
| 'CEventNetworkPlayerEnteredVehicle'
|
|
||||||
| 'CEventNetworkPlayerJoinScript'
|
|
||||||
| 'CEventNetworkPlayerLeftScript'
|
|
||||||
| 'CEventNetworkPlayerScript'
|
|
||||||
| 'CEventNetworkPlayerSession'
|
|
||||||
| 'CEventNetworkPlayerSpawn'
|
|
||||||
| 'CEventNetworkPresenceInvite'
|
|
||||||
| 'CEventNetworkPresenceInviteRemoved'
|
|
||||||
| 'CEventNetworkPresenceInviteReply'
|
|
||||||
| 'CEventNetworkPresenceTriggerEvent'
|
|
||||||
| 'CEventNetworkPresence_StatUpdate'
|
|
||||||
| 'CEventNetworkPrimaryClanChanged'
|
|
||||||
| 'CEventNetworkRequestDelay'
|
|
||||||
| 'CEventNetworkRosChanged'
|
|
||||||
| 'CEventNetworkScAdminPlayerUpdated'
|
|
||||||
| 'CEventNetworkScAdminReceivedCash'
|
|
||||||
| 'CEventNetworkScriptEvent'
|
|
||||||
| 'CEventNetworkSessionEvent'
|
|
||||||
| 'CEventNetworkShopTransaction'
|
|
||||||
| 'CEventNetworkSignInStateChanged'
|
|
||||||
| 'CEventNetworkSocialClubAccountLinked'
|
|
||||||
| 'CEventNetworkSpectateLocal'
|
|
||||||
| 'CEventNetworkStartMatch'
|
|
||||||
| 'CEventNetworkStartSession'
|
|
||||||
| 'CEventNetworkStorePlayerLeft'
|
|
||||||
| 'CEventNetworkSummon'
|
|
||||||
| 'CEventNetworkSystemServiceEvent'
|
|
||||||
| 'CEventNetworkTextMessageReceived'
|
|
||||||
| 'CEventNetworkTimedExplosion'
|
|
||||||
| 'CEventNetworkTransitionEvent'
|
|
||||||
| 'CEventNetworkTransitionGamerInstruction'
|
|
||||||
| 'CEventNetworkTransitionMemberJoined'
|
|
||||||
| 'CEventNetworkTransitionMemberLeft'
|
|
||||||
| 'CEventNetworkTransitionParameterChanged'
|
|
||||||
| 'CEventNetworkTransitionStarted'
|
|
||||||
| 'CEventNetworkTransitionStringChanged'
|
|
||||||
| 'CEventNetworkVehicleUndrivable'
|
|
||||||
| 'CEventNetworkVoiceConnectionRequested'
|
|
||||||
| 'CEventNetworkVoiceConnectionResponse'
|
|
||||||
| 'CEventNetworkVoiceConnectionTerminated'
|
|
||||||
| 'CEventNetworkVoiceSessionEnded'
|
|
||||||
| 'CEventNetworkVoiceSessionStarted'
|
|
||||||
| 'CEventNetworkWithData'
|
|
||||||
| 'CEventNetwork_InboxMsgReceived'
|
|
||||||
| 'CEventNewTask'
|
|
||||||
| 'CEventObjectCollision'
|
|
||||||
| 'CEventOnFire'
|
|
||||||
| 'CEventOpenDoor'
|
|
||||||
| 'CEventPedCollisionWithPed'
|
|
||||||
| 'CEventPedCollisionWithPlayer'
|
|
||||||
| 'CEventPedEnteredMyVehicle'
|
|
||||||
| 'CEventPedJackingMyVehicle'
|
|
||||||
| 'CEventPedOnCarRoof'
|
|
||||||
| 'CEventPedSeenDeadPed'
|
|
||||||
| 'CEventPlayerCollisionWithPed'
|
|
||||||
| 'CEventPlayerDeath'
|
|
||||||
| 'CEventPlayerUnableToEnterVehicle'
|
|
||||||
| 'CEventPotentialBeWalkedInto'
|
|
||||||
| 'CEventPotentialBlast'
|
|
||||||
| 'CEventPotentialGetRunOver'
|
|
||||||
| 'CEventPotentialWalkIntoVehicle'
|
|
||||||
| 'CEventProvidingCover'
|
|
||||||
| 'CEventRanOverPed'
|
|
||||||
| 'CEventReactionEnemyPed'
|
|
||||||
| 'CEventReactionInvestigateDeadPed'
|
|
||||||
| 'CEventReactionInvestigateThreat'
|
|
||||||
| 'CEventRequestHelp'
|
|
||||||
| 'CEventRequestHelpWithConfrontation'
|
|
||||||
| 'CEventRespondedToThreat'
|
|
||||||
| 'CEventScanner'
|
|
||||||
| 'CEventScenarioForceAction'
|
|
||||||
| 'CEventScriptCommand'
|
|
||||||
| 'CEventScriptWithData'
|
|
||||||
| 'CEventShocking'
|
|
||||||
| 'CEventShockingBicycleCrash'
|
|
||||||
| 'CEventShockingBicycleOnPavement'
|
|
||||||
| 'CEventShockingCarAlarm'
|
|
||||||
| 'CEventShockingCarChase'
|
|
||||||
| 'CEventShockingCarCrash'
|
|
||||||
| 'CEventShockingCarOnCar'
|
|
||||||
| 'CEventShockingCarPileUp'
|
|
||||||
| 'CEventShockingDangerousAnimal'
|
|
||||||
| 'CEventShockingDeadBody'
|
|
||||||
| 'CEventShockingDrivingOnPavement'
|
|
||||||
| 'CEventShockingEngineRevved'
|
|
||||||
| 'CEventShockingExplosion'
|
|
||||||
| 'CEventShockingFire'
|
|
||||||
| 'CEventShockingGunFight'
|
|
||||||
| 'CEventShockingGunshotFired'
|
|
||||||
| 'CEventShockingHelicopterOverhead'
|
|
||||||
| 'CEventShockingHornSounded'
|
|
||||||
| 'CEventShockingInDangerousVehicle'
|
|
||||||
| 'CEventShockingInjuredPed'
|
|
||||||
| 'CEventShockingMadDriver'
|
|
||||||
| 'CEventShockingMadDriverBicycle'
|
|
||||||
| 'CEventShockingMadDriverExtreme'
|
|
||||||
| 'CEventShockingMugging'
|
|
||||||
| 'CEventShockingNonViolentWeaponAimedAt'
|
|
||||||
| 'CEventShockingParachuterOverhead'
|
|
||||||
| 'CEventShockingPedKnockedIntoByPlayer'
|
|
||||||
| 'CEventShockingPedRunOver'
|
|
||||||
| 'CEventShockingPedShot'
|
|
||||||
| 'CEventShockingPlaneFlyby'
|
|
||||||
| 'CEventShockingPotentialBlast'
|
|
||||||
| 'CEventShockingPropertyDamage'
|
|
||||||
| 'CEventShockingRunningPed'
|
|
||||||
| 'CEventShockingRunningStampede'
|
|
||||||
| 'CEventShockingSeenCarStolen'
|
|
||||||
| 'CEventShockingSeenConfrontation'
|
|
||||||
| 'CEventShockingSeenGangFight'
|
|
||||||
| 'CEventShockingSeenInsult'
|
|
||||||
| 'CEventShockingSeenMeleeAction'
|
|
||||||
| 'CEventShockingSeenNiceCar'
|
|
||||||
| 'CEventShockingSeenPedKilled'
|
|
||||||
| 'CEventShockingSiren'
|
|
||||||
| 'CEventShockingStudioBomb'
|
|
||||||
| 'CEventShockingVehicleTowed'
|
|
||||||
| 'CEventShockingVisibleWeapon'
|
|
||||||
| 'CEventShockingWeaponThreat'
|
|
||||||
| 'CEventShockingWeirdPed'
|
|
||||||
| 'CEventShockingWeirdPedApproaching'
|
|
||||||
| 'CEventShoutBlockingLos'
|
|
||||||
| 'CEventShoutTargetPosition'
|
|
||||||
| 'CEventShovePed'
|
|
||||||
| 'CEventSoundBase'
|
|
||||||
| 'CEventStatChangedValue'
|
|
||||||
| 'CEventStaticCountReachedMax'
|
|
||||||
| 'CEventStuckInAir'
|
|
||||||
| 'CEventSuspiciousActivity'
|
|
||||||
| 'CEventSwitch2NM'
|
|
||||||
| 'CEventUnidentifiedPed'
|
|
||||||
| 'CEventVehicleCollision'
|
|
||||||
| 'CEventVehicleDamage'
|
|
||||||
| 'CEventVehicleDamageWeapon'
|
|
||||||
| 'CEventVehicleOnFire'
|
|
||||||
| 'CEventWrithe'
|
|
||||||
|
|||||||
@@ -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<T> = 0 extends 1 & T ? true : false
|
||||||
|
|
||||||
|
/** The declared map, or a loose one when nothing is declared */
|
||||||
|
type EventMap<T> =
|
||||||
|
IsAny<T> extends true
|
||||||
|
? Record<string, AnyEvent>
|
||||||
|
: [keyof T] extends [never]
|
||||||
|
? Record<string, AnyEvent>
|
||||||
|
: T
|
||||||
|
|
||||||
|
export type RPCEventName<T> = keyof EventMap<T> & string
|
||||||
|
|
||||||
|
export type RPCEventArgs<
|
||||||
|
T,
|
||||||
|
K extends RPCEventName<T>,
|
||||||
|
> = EventMap<T>[K] extends (...args: infer A extends unknown[]) => unknown
|
||||||
|
? A
|
||||||
|
: never
|
||||||
|
|
||||||
|
export type RPCEventResult<
|
||||||
|
T,
|
||||||
|
K extends RPCEventName<T>,
|
||||||
|
> = EventMap<T>[K] extends (...args: never[]) => infer R ? Awaited<R> : never
|
||||||
|
|
||||||
|
/** Listener for event `K` of map `T`; `Prefix` goes before the event arguments */
|
||||||
|
export type RPCListener<
|
||||||
|
T,
|
||||||
|
K extends RPCEventName<T>,
|
||||||
|
Prefix extends unknown[] = [],
|
||||||
|
> = (
|
||||||
|
...args: [...Prefix, ...RPCEventArgs<T, K>]
|
||||||
|
) => RPCEventResult<T, K> | Promise<RPCEventResult<T, K>>
|
||||||
|
|
||||||
|
export type RPCCommandName<T> = [keyof T] extends [never]
|
||||||
|
? string
|
||||||
|
: keyof T & string
|
||||||
+9
-13
@@ -1,19 +1,15 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es6",
|
"target": "es2022",
|
||||||
"module": "commonjs",
|
"lib": ["es2022", "dom"],
|
||||||
"moduleResolution": "node",
|
"module": "preserve",
|
||||||
"lib": ["ES6", "dom"],
|
"moduleResolution": "bundler",
|
||||||
"declaration": true,
|
|
||||||
"declarationMap": true,
|
|
||||||
"sourceMap": true,
|
|
||||||
|
|
||||||
"outDir": "dist",
|
"noEmit": true,
|
||||||
"esModuleInterop": true,
|
"isolatedModules": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
|
||||||
"strict": true,
|
"strict": true
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"noImplicitAny": true
|
|
||||||
},
|
},
|
||||||
"include": ["src/**/*"]
|
"include": ["src"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { defineConfig } from 'tsdown'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
entry: ['src/index.ts'],
|
||||||
|
format: ['esm', 'cjs'],
|
||||||
|
platform: 'neutral',
|
||||||
|
target: 'es2022',
|
||||||
|
fixedExtension: true,
|
||||||
|
dts: true,
|
||||||
|
exports: true,
|
||||||
|
})
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import { defineConfig } from 'tsup'
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
entry: ['src/index.ts'],
|
|
||||||
outDir: './dist',
|
|
||||||
target: 'node16',
|
|
||||||
platform: 'node',
|
|
||||||
format: ['cjs'],
|
|
||||||
splitting: false,
|
|
||||||
sourcemap: false,
|
|
||||||
clean: false,
|
|
||||||
experimentalDts: true,
|
|
||||||
noExternal: [/.*/],
|
|
||||||
})
|
|
||||||
Vendored
+58
@@ -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 {}
|
||||||
+35
-14
@@ -1,30 +1,51 @@
|
|||||||
{
|
{
|
||||||
"name": "@entityseven/fivem-rpc-shared-types",
|
"name": "@entityseven/fivem-rpc-shared-types",
|
||||||
"description": "Shared types for @entityseven/fivem-rpc. Highly recommended to install together",
|
"version": "1.0.0",
|
||||||
"version": "0.1.0",
|
"description": "Event and command declarations for @entityseven/fivem-rpc, for typed events",
|
||||||
"types": "types/types/index.d.ts",
|
|
||||||
"files": [
|
|
||||||
"types/**/*",
|
|
||||||
"readme.md",
|
|
||||||
"license.md"
|
|
||||||
],
|
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"fivem-rpc-shared-types",
|
"cfx",
|
||||||
"fivem-rpc",
|
|
||||||
"fivem",
|
"fivem",
|
||||||
"gta"
|
"fivem-rpc",
|
||||||
|
"types",
|
||||||
|
"typescript"
|
||||||
],
|
],
|
||||||
"type": "module",
|
"license": "SEE LICENSE IN license.md",
|
||||||
"author": "Entity Seven Group",
|
"author": "Entity Seven Group",
|
||||||
"contributors": [
|
"contributors": [
|
||||||
{
|
{
|
||||||
"name": "Danya H",
|
"name": "Danya H",
|
||||||
"email": "[email protected]",
|
"email": "[email protected]",
|
||||||
"url": "https://github.com/rilaxik/"
|
"url": "https://github.com/rilaxik/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Oleksandr Honcharov",
|
||||||
|
"email": "[email protected]",
|
||||||
|
"url": "https://github.com/SashaGoncharov19/"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "Custom-Attribution-NoDerivs",
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/rilaxik/fivem-rpc.git",
|
||||||
|
"directory": "shared-types"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"index.d.ts",
|
||||||
|
"readme.md",
|
||||||
|
"license.md"
|
||||||
|
],
|
||||||
|
"types": "./index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./index.d.ts"
|
||||||
|
},
|
||||||
|
"./package.json": "./package.json"
|
||||||
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"typescript": "^5"
|
"typescript": ">=5"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"typescript": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
# FiveM RPC Shared Types
|
||||||
|
|
||||||
|
Event and command declarations for [`@entityseven/fivem-rpc`](../rpc/readme.md). With nothing declared, every rpc method accepts any event name, any arguments and any result. Declare your events once and every `on*` and `emit*` call gets checked names, arguments and results
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
See the [main readme](../readme.md#installation). The declaration file below imports this package, so it must resolve from that file (in a workspace: install it in the root)
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
1. Create one declaration file shared by server, client and webview code, e.g. `shared/rpc.d.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import '@entityseven/fivem-rpc-shared-types'
|
||||||
|
|
||||||
|
declare module '@entityseven/fivem-rpc-shared-types' {
|
||||||
|
interface RPCEvents_ClientServer {
|
||||||
|
// event name(arguments): value returned by the listener
|
||||||
|
buyItem(item: string, amount: number): boolean
|
||||||
|
}
|
||||||
|
interface RPCCommands_Server {
|
||||||
|
ban: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `import` line makes the file a module, so `declare module` adds to the package's interfaces instead of replacing them
|
||||||
|
|
||||||
|
2. Add the file to `include` in the `tsconfig.json` of every environment:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"include": ["src", "../shared/rpc.d.ts"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Calls are checked from now on:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// server
|
||||||
|
rpc.onClient('buyItem', (player, item, amount) => {
|
||||||
|
// item: string, amount: number, must return boolean
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
// client
|
||||||
|
const bought = await rpc.emitServer('buyItem', 'water', 2) // boolean
|
||||||
|
await rpc.emitServer('buyItem', 'water') // error: missing `amount`
|
||||||
|
await rpc.emitServer('buyItme', 'water', 2) // error: unknown event
|
||||||
|
```
|
||||||
|
|
||||||
|
## Interfaces
|
||||||
|
|
||||||
|
Each interface types one direction, see the [direction table](../rpc/readme.md#directions) for which `emit*` and `on*` methods use it. `RPCCommands_Server` and `RPCCommands_Client` type `onCommand`. An interface you leave empty stays loose (any name, arguments and result), so you can declare them one at a time
|
||||||
|
|
||||||
|
- 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
|
||||||
|
|
||||||
|
Licensed under the [Custom Attribution-NoDerivs Software License](license.md)
|
||||||
|
|||||||
Vendored
-52
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user