mirror of
https://github.com/rilaxik/fivem-rpc.git
synced 2026-09-28 01:29:13 +00:00
Compare commits
39
Commits
821cd589d9
...
v1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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
|
||||
dist
|
||||
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,
|
||||
"type": "module",
|
||||
"license": "CC0-1.0",
|
||||
"license": "SEE LICENSE IN license.md",
|
||||
"author": "Entity Seven Group",
|
||||
"contributors": [
|
||||
{
|
||||
@@ -33,5 +17,23 @@
|
||||
"repository": {
|
||||
"type": "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",
|
||||
"description": "FiveM RPC is an abstraction for events in GTA V FiveM servers in JS/TS",
|
||||
"version": "0.1.0",
|
||||
"main": "",
|
||||
"types": "",
|
||||
"files": [
|
||||
"types/**/*",
|
||||
"readme.md",
|
||||
"license.md"
|
||||
],
|
||||
"version": "1.0.0",
|
||||
"description": "Call FiveM server, client and NUI listeners like async functions: typed, with timeouts, no event ping-pong",
|
||||
"keywords": [
|
||||
"fivem-rpc",
|
||||
"fivem-rpc-shared-types",
|
||||
"cfx",
|
||||
"fivem",
|
||||
"gta"
|
||||
"fivem-rpc",
|
||||
"gta",
|
||||
"nui",
|
||||
"rpc",
|
||||
"typescript"
|
||||
],
|
||||
"type": "module",
|
||||
"license": "SEE LICENSE IN license.md",
|
||||
"author": "Entity Seven Group",
|
||||
"contributors": [
|
||||
{
|
||||
"name": "Danya H",
|
||||
"email": "[email protected]",
|
||||
"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": {
|
||||
"@microsoft/api-extractor": "^7.47.9",
|
||||
"@citizenfx/client": "^2.0.15015-1",
|
||||
"@citizenfx/server": "^2.0.14862-1",
|
||||
"tsup": "^8.3.0"
|
||||
"tsdown": "^0.23.0"
|
||||
},
|
||||
"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 { 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 {
|
||||
NATIVE_CLIENT_EVENTS,
|
||||
NATIVE_CLIENT_NETWORK_EVENTS,
|
||||
@@ -12,38 +12,57 @@ import {
|
||||
RPCErrors,
|
||||
RPCEvents,
|
||||
type RPCNativeClientEvents,
|
||||
type RPCNativeClientNetworksEvents,
|
||||
type RPCNativeClientNetworkEvents,
|
||||
type RPCState,
|
||||
type RPCStateRaw,
|
||||
type RPCStateWeb,
|
||||
} 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 _pendingServer: Emitter
|
||||
private readonly _emitterWeb: Emitter
|
||||
private readonly _pendingWeb: Emitter
|
||||
private readonly _pendingWebToServer: Emitter
|
||||
|
||||
constructor(props: RPCConfig<'client'>) {
|
||||
super(props)
|
||||
|
||||
this._emitterServer = new Emitter()
|
||||
this._pendingServer = 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))
|
||||
RegisterNuiCallbackType(RPCEvents.LISTENER_WEB)
|
||||
on(
|
||||
`__cfx_nui:${RPCEvents.LISTENER_WEB}`,
|
||||
async (data: RPCState, callback: (res: unknown) => void) => {
|
||||
const res = await this._handleWeb(data)
|
||||
callback(res)
|
||||
async (data: unknown, callback: (res: unknown) => void) => {
|
||||
const payload = this.accept(data)
|
||||
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 =====
|
||||
|
||||
private async _handleServer(payloadRaw: RPCStateRaw) {
|
||||
try {
|
||||
parse(payloadRaw)
|
||||
} catch (e) {
|
||||
throw new Error(RPCErrors.INVALID_DATA)
|
||||
}
|
||||
const payload = parse(payloadRaw)
|
||||
const payload = this.accept(payloadRaw)
|
||||
if (!payload) return
|
||||
|
||||
if (this.debug) {
|
||||
this.console.log(
|
||||
`[RPC]:client:accepted ${payload.type} ${payload.event} from ${payload.calledFrom}`,
|
||||
)
|
||||
}
|
||||
if (payload.type === 'event' || payload.type === 'broadcast') {
|
||||
if (payload.calledTo === 'client') {
|
||||
const response = await this.dispatch(this._emitterServer, payload)
|
||||
|
||||
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))
|
||||
} 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') {
|
||||
this._sendWebMessage({
|
||||
@@ -95,55 +95,36 @@ export class RPCInstanceClient extends Wrapper {
|
||||
}
|
||||
if (payload.type === 'response') {
|
||||
if (payload.calledTo === 'client') {
|
||||
await this._pendingServer.emit(
|
||||
payload.uuid,
|
||||
...(payload.data && payload.data.length > 0 ? payload.data : []),
|
||||
)
|
||||
this.settle(payload)
|
||||
}
|
||||
if (payload.calledTo === 'webview') {
|
||||
await this._pendingWebToServer.emit(
|
||||
payload.uuid,
|
||||
...(payload.data && payload.data.length > 0 ? payload.data : []),
|
||||
)
|
||||
// relayed webview -> server call: the webview gets the whole response
|
||||
if (!this._pending.resolve(payload.uuid, payload)) {
|
||||
this.logIgnored(payload)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.calledTo === 'client') {
|
||||
return await this._emitterWeb.emit(
|
||||
payload.event,
|
||||
...(payload.data && payload.data.length > 0 ? payload.data : []),
|
||||
)
|
||||
return this.dispatch(this._emitterWeb, payload)
|
||||
}
|
||||
if (payload.calledTo === 'server') {
|
||||
payload.player = GetPlayerServerId(PlayerId())
|
||||
emitNet(RPCEvents.LISTENER_WEB, stringify(payload))
|
||||
|
||||
return new Promise(res => {
|
||||
this._pendingWebToServer.once(payload.uuid, res)
|
||||
})
|
||||
return this._pending.wait<RPCState>(payload)
|
||||
}
|
||||
}
|
||||
|
||||
if (payload.type === 'response') {
|
||||
if (payload.calledTo === 'client') {
|
||||
await this._pendingWeb.emit(
|
||||
payload.uuid,
|
||||
...(payload.data && payload.data.length > 0 ? payload.data : []),
|
||||
)
|
||||
this.settle(payload)
|
||||
|
||||
return { status: 'ok' }
|
||||
}
|
||||
if (payload.calledTo === 'server') {
|
||||
payload.player = GetPlayerServerId(PlayerId())
|
||||
emitNet(RPCEvents.LISTENER_WEB, stringify(payload))
|
||||
|
||||
return { status: 'ok' }
|
||||
@@ -154,240 +135,237 @@ export class RPCInstanceClient extends Wrapper {
|
||||
|
||||
// ===== SERVER =====
|
||||
|
||||
public onServer<
|
||||
EventName extends keyof s.RPCEvents_ServerClient,
|
||||
CallbackArguments extends Parameters<s.RPCEvents_ServerClient[EventName]>,
|
||||
CallbackReturn extends ReturnType<s.RPCEvents_ServerClient[EventName]>,
|
||||
/**
|
||||
* Listens for `emitClient` and `emitClientEveryone` calls from the server
|
||||
* (server -> client). For `emitClientEveryone` the return value is not sent.
|
||||
*
|
||||
* @param cb - gets the event arguments. Its return value (awaited) is sent
|
||||
* back to the caller
|
||||
*
|
||||
* @example
|
||||
* rpc.onServer('askTrade', offer => showTradeDialog(offer))
|
||||
*/
|
||||
public onServer<EventName 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,
|
||||
cb: (
|
||||
...args: CallbackArguments
|
||||
) => Awaited<CallbackReturn> | Promise<Awaited<CallbackReturn>>,
|
||||
): 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',
|
||||
}
|
||||
...args: RPCEventArgs<s.RPCEvents_ClientServer, EventName>
|
||||
): Promise<RPCEventResult<s.RPCEvents_ClientServer, EventName>> {
|
||||
const payload = this.request(eventName, 'server', args, null)
|
||||
|
||||
emitNet(RPCEvents.LISTENER_CLIENT, stringify(payload))
|
||||
|
||||
return new Promise<Awaited<Response>>(res => {
|
||||
this._pendingServer.once(payload.uuid, res)
|
||||
})
|
||||
return this._pending.wait(payload)
|
||||
}
|
||||
|
||||
// ===== WEBVIEW =====
|
||||
|
||||
public onWebview<
|
||||
EventName extends keyof s.RPCEvents_WebviewClient,
|
||||
CallbackArguments extends Parameters<s.RPCEvents_WebviewClient[EventName]>,
|
||||
CallbackReturn extends ReturnType<s.RPCEvents_WebviewClient[EventName]>,
|
||||
/**
|
||||
* Listens for `emitClient` calls from this player's webview
|
||||
* (webview -> client).
|
||||
*
|
||||
* @param cb - gets the event arguments. Its return value (awaited) is sent
|
||||
* back to the caller
|
||||
*
|
||||
* @example
|
||||
* rpc.onWebview('getPosition', () => GetEntityCoords(PlayerPedId(), false))
|
||||
*/
|
||||
public onWebview<EventName 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,
|
||||
cb: (
|
||||
...args: CallbackArguments
|
||||
) => Awaited<CallbackReturn> | Promise<Awaited<CallbackReturn>>,
|
||||
): 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',
|
||||
}
|
||||
...args: RPCEventArgs<s.RPCEvents_ClientWebview, EventName>
|
||||
): Promise<RPCEventResult<s.RPCEvents_ClientWebview, EventName>> {
|
||||
const payload = this.request(eventName, 'webview', args, null)
|
||||
|
||||
this._sendWebMessage({
|
||||
origin: RPCEvents.LISTENER_CLIENT,
|
||||
data: payload,
|
||||
})
|
||||
|
||||
return new Promise<Awaited<Response>>(res => {
|
||||
this._pendingWeb.once(payload.uuid, res)
|
||||
})
|
||||
return this._pending.wait(payload)
|
||||
}
|
||||
|
||||
// ===== SELF =====
|
||||
|
||||
public onSelf<
|
||||
EventName extends keyof s.RPCEvents_Client,
|
||||
CallbackArguments extends Parameters<s.RPCEvents_Client[EventName]>,
|
||||
CallbackReturn extends ReturnType<s.RPCEvents_Client[EventName]>,
|
||||
>(
|
||||
/**
|
||||
* Listens for `emitSelf` calls in this environment (client -> client).
|
||||
*
|
||||
* @param cb - gets the event arguments. Its return value (awaited) is sent
|
||||
* back to the caller
|
||||
*
|
||||
* @example
|
||||
* rpc.onSelf('add', (a, b) => a + b)
|
||||
*/
|
||||
public onSelf<EventName extends RPCEventName<s.RPCEvents_Client>>(
|
||||
eventName: EventName,
|
||||
cb: (
|
||||
...args: CallbackArguments
|
||||
) => Awaited<CallbackReturn> | Promise<Awaited<CallbackReturn>>,
|
||||
cb: RPCListener<s.RPCEvents_Client, EventName>,
|
||||
): this {
|
||||
if (this.debug) {
|
||||
this.console.log(`[RPC]:onSelf ${eventName}`)
|
||||
return this.listen(this._emitterLocal, 'onSelf', eventName, cb)
|
||||
}
|
||||
|
||||
this._emitterLocal.on(eventName, cb)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public offSelf<EventName extends keyof s.RPCEvents_Client>(
|
||||
/** Removes the `onSelf` listener for `eventName` */
|
||||
public offSelf<EventName extends RPCEventName<s.RPCEvents_Client>>(
|
||||
eventName: EventName,
|
||||
): this {
|
||||
if (this.debug) {
|
||||
this.console.log(`[RPC]:offSelf ${eventName}`)
|
||||
return this.unlisten(this._emitterLocal, 'offSelf', eventName)
|
||||
}
|
||||
|
||||
this._emitterLocal.off(eventName)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public async emitSelf<
|
||||
EventName extends keyof s.RPCEvents_Client,
|
||||
Arguments extends Parameters<s.RPCEvents_Client[EventName]>,
|
||||
Response extends ReturnType<s.RPCEvents_Client[EventName]>,
|
||||
>(eventName: EventName, ...args: Arguments): Promise<Awaited<Response>> {
|
||||
const payload: RPCState = {
|
||||
event: eventName,
|
||||
uuid: generateUUID(),
|
||||
calledFrom: 'client',
|
||||
calledTo: 'client',
|
||||
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 : []),
|
||||
)
|
||||
/**
|
||||
* Calls this environment's own `onSelf` listener directly and resolves with
|
||||
* its return value. No timeout; errors thrown by the listener reach the caller
|
||||
* unchanged.
|
||||
*
|
||||
* @throws {@link RPCError} `EVENT_NOT_REGISTERED` if no `onSelf` listener exists
|
||||
*
|
||||
* @example
|
||||
* const total = await rpc.emitSelf('add', 2, 3)
|
||||
*/
|
||||
public async emitSelf<EventName extends RPCEventName<s.RPCEvents_Client>>(
|
||||
eventName: EventName,
|
||||
...args: RPCEventArgs<s.RPCEvents_Client, EventName>
|
||||
): Promise<RPCEventResult<s.RPCEvents_Client, EventName>> {
|
||||
return this.emitLocal(eventName, args)
|
||||
}
|
||||
|
||||
// ===== OTHER =====
|
||||
|
||||
public onCommand<
|
||||
CommandName extends s.RPCCommands_Client,
|
||||
CallbackArguments extends unknown[],
|
||||
>(
|
||||
/**
|
||||
* Registers a chat command (FiveM `RegisterCommand`).
|
||||
*
|
||||
* @param cb - gets FiveM's `source`, the strings typed after the command and
|
||||
* the full command line. Validate `args` yourself
|
||||
*
|
||||
* @example
|
||||
* rpc.onCommand('coords', () => console.log(GetEntityCoords(PlayerPedId(), false)))
|
||||
*/
|
||||
public onCommand<CommandName extends RPCCommandName<s.RPCCommands_Client>>(
|
||||
command: CommandName,
|
||||
cb: (player: number, args: CallbackArguments, commandRaw: string) => void,
|
||||
cb: (player: number, args: string[], rawCommand: string) => void,
|
||||
): this {
|
||||
if (this.debug) {
|
||||
this.console.log(`[RPC]:onCommand ${command}`)
|
||||
}
|
||||
this.log(`onCommand ${command}`)
|
||||
|
||||
RegisterCommand(command, cb, false)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public onNativeEvent<
|
||||
EventName extends keyof RPCNativeClientEvents,
|
||||
CallbackArguments extends Parameters<RPCNativeClientEvents[EventName]>,
|
||||
>(eventName: EventName, cb: (...args: CallbackArguments) => void): this {
|
||||
/**
|
||||
* Listens to a native FiveM client event, e.g. `entityDamaged`.
|
||||
*
|
||||
* @throws {@link RPCError} `UNKNOWN_NATIVE` if `eventName` is not in
|
||||
* `NATIVE_CLIENT_EVENTS`. Register other events with FiveM's `on` directly
|
||||
*
|
||||
* @example
|
||||
* rpc.onNativeEvent('entityDamaged', (victim, culprit) => console.log(victim))
|
||||
*/
|
||||
public onNativeEvent<EventName extends keyof RPCNativeClientEvents>(
|
||||
eventName: EventName,
|
||||
cb: (...args: Parameters<RPCNativeClientEvents[EventName]>) => void,
|
||||
): this {
|
||||
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.console.log(`[RPC]:onNativeEvent ${eventName}`)
|
||||
}
|
||||
this.log(`onNativeEvent ${eventName}`)
|
||||
|
||||
on(eventName, cb)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Listens to a native game event, e.g. `CEventShockingCarCrash`.
|
||||
*
|
||||
* @throws {@link RPCError} `UNKNOWN_NATIVE` if `eventName` is not in
|
||||
* `NATIVE_CLIENT_NETWORK_EVENTS`. Register other events with FiveM's `on`
|
||||
* directly
|
||||
*
|
||||
* @example
|
||||
* rpc.onNativeNetworkEvent('CEventShockingCarCrash', (entities, eventEntity) => {})
|
||||
*/
|
||||
public onNativeNetworkEvent<
|
||||
EventName extends keyof RPCNativeClientNetworksEvents,
|
||||
CallbackArguments extends Parameters<
|
||||
RPCNativeClientNetworksEvents[EventName]
|
||||
>,
|
||||
>(eventName: EventName, cb: (...args: CallbackArguments) => void): this {
|
||||
EventName extends keyof RPCNativeClientNetworkEvents,
|
||||
>(
|
||||
eventName: EventName,
|
||||
cb: (...args: Parameters<RPCNativeClientNetworkEvents[EventName]>) => void,
|
||||
): this {
|
||||
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.console.log(`[RPC]:onNativeNetworkEvent ${eventName}`)
|
||||
}
|
||||
this.log(`onNativeNetworkEvent ${eventName}`)
|
||||
|
||||
on(eventName, cb)
|
||||
|
||||
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 {
|
||||
if (this.debug) {
|
||||
this.console.log(`[RPC]:setWebviewFocus ${hasFocus} ${hasCursor}`)
|
||||
}
|
||||
this.log(`setWebviewFocus ${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 { 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 {
|
||||
type RPCConfig,
|
||||
RPCErrors,
|
||||
RPCEvents,
|
||||
type RPCNativeServerEvents,
|
||||
type RPCState,
|
||||
type RPCStateRaw,
|
||||
} 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 _pendingClient: Emitter
|
||||
private readonly _emitterWeb: Emitter
|
||||
private readonly _pendingWeb: Emitter
|
||||
|
||||
constructor(props: RPCConfig<'server'>) {
|
||||
super(props)
|
||||
|
||||
this._emitterClient = new Emitter()
|
||||
this._pendingClient = 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))
|
||||
onNet(RPCEvents.LISTENER_WEB, this._handleWeb.bind(this))
|
||||
// `source` must be read synchronously, before any await
|
||||
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 =====
|
||||
|
||||
private async _handleClient(payloadRaw: RPCStateRaw) {
|
||||
try {
|
||||
parse(payloadRaw)
|
||||
} catch (e) {
|
||||
throw new Error(RPCErrors.INVALID_DATA)
|
||||
}
|
||||
const payload = parse(payloadRaw)
|
||||
/**
|
||||
* Handles a payload from a client or its webview.
|
||||
*
|
||||
* @param player - FiveM `source` of the net event: the only trusted player id,
|
||||
* whatever the payload claims
|
||||
*/
|
||||
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) {
|
||||
this.console.log(
|
||||
`[RPC]:server:accepted ${payload.type} ${payload.event} from ${payload.calledFrom}`,
|
||||
)
|
||||
}
|
||||
|
||||
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)
|
||||
// not sent by a player, e.g. a server-side trigger of the RPC channel
|
||||
if (!(player > 0)) {
|
||||
this.log(`dropped ${payload.event}: no player source`)
|
||||
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') {
|
||||
this.verifyEvent(this._emitterWeb, payload)
|
||||
if (payload.player === null || payload.player === -1) {
|
||||
payload.error = RPCErrors.NO_PLAYER
|
||||
this.triggerError(payload)
|
||||
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 : []),
|
||||
)
|
||||
}
|
||||
const response = await this.dispatch(emitter, payload, player)
|
||||
emitNet(RPCEvents.LISTENER_SERVER, player, stringify(response))
|
||||
} else if (payload.type === 'response') {
|
||||
this.settle(payload, player)
|
||||
}
|
||||
}
|
||||
|
||||
// ===== CLIENT =====
|
||||
|
||||
public onClient<
|
||||
EventName extends keyof s.RPCEvents_ClientServer,
|
||||
CallbackArguments extends Parameters<s.RPCEvents_ClientServer[EventName]>,
|
||||
CallbackReturn extends ReturnType<s.RPCEvents_ClientServer[EventName]>,
|
||||
>(
|
||||
/**
|
||||
* Listens for `emitServer` calls from clients (client -> server).
|
||||
*
|
||||
* @param cb - gets the calling player's server id first (from FiveM
|
||||
* `source`, never from the payload), then the event arguments. Its return
|
||||
* value (awaited) is sent back to the caller
|
||||
*
|
||||
* @example
|
||||
* rpc.onClient('getMoney', (player, account) => getMoney(player, account))
|
||||
*/
|
||||
public onClient<EventName extends RPCEventName<s.RPCEvents_ClientServer>>(
|
||||
eventName: EventName,
|
||||
cb: (
|
||||
player: number,
|
||||
...args: CallbackArguments
|
||||
) => Awaited<CallbackReturn> | Promise<Awaited<CallbackReturn>>,
|
||||
cb: RPCListener<s.RPCEvents_ClientServer, EventName, [player: number]>,
|
||||
): this {
|
||||
if (this.debug) {
|
||||
this.console.log(`[RPC]:onClient ${eventName}`)
|
||||
return this.listen(this._emitterClient, 'onClient', eventName, cb)
|
||||
}
|
||||
|
||||
this._emitterClient.on(eventName, cb)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public offClient<EventName extends keyof s.RPCEvents_ClientServer>(
|
||||
/** Removes the `onClient` listener for `eventName` */
|
||||
public offClient<EventName extends RPCEventName<s.RPCEvents_ClientServer>>(
|
||||
eventName: EventName,
|
||||
): this {
|
||||
if (this.debug) {
|
||||
this.console.log(`[RPC]:offClient ${eventName}`)
|
||||
}
|
||||
|
||||
this._emitterClient.off(eventName)
|
||||
|
||||
return this
|
||||
return this.unlisten(this._emitterClient, 'offClient', eventName)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the `onServer` listener on one client (server -> client) and
|
||||
* resolves with its return value.
|
||||
*
|
||||
* @param player - server id of the target player
|
||||
* @throws {@link RPCError} `EVENT_NOT_REGISTERED` (no listener),
|
||||
* `HANDLER_ERROR` (the listener threw) or `TIMEOUT`
|
||||
*
|
||||
* @example
|
||||
* const accepted = await rpc.emitClient(player, 'askTrade', offer)
|
||||
*/
|
||||
public async emitClient<
|
||||
EventName extends keyof s.RPCEvents_ServerClient,
|
||||
Arguments extends Parameters<s.RPCEvents_ServerClient[EventName]>,
|
||||
Response extends ReturnType<s.RPCEvents_ServerClient[EventName]>,
|
||||
EventName extends RPCEventName<s.RPCEvents_ServerClient>,
|
||||
>(
|
||||
player: number,
|
||||
eventName: EventName,
|
||||
...args: Arguments
|
||||
): Promise<Awaited<Response>> {
|
||||
const payload: RPCState = {
|
||||
event: eventName,
|
||||
uuid: generateUUID(),
|
||||
calledFrom: 'server',
|
||||
calledTo: 'client',
|
||||
error: null,
|
||||
data: args.length ? args : null,
|
||||
player: player,
|
||||
type: 'event',
|
||||
}
|
||||
...args: RPCEventArgs<s.RPCEvents_ServerClient, EventName>
|
||||
): Promise<RPCEventResult<s.RPCEvents_ServerClient, EventName>> {
|
||||
const payload = this.request(eventName, 'client', args, player)
|
||||
|
||||
emitNet(RPCEvents.LISTENER_SERVER, player, stringify(payload))
|
||||
|
||||
return new Promise<Awaited<Response>>(res => {
|
||||
this._pendingClient.once(payload.uuid, res)
|
||||
})
|
||||
return this._pending.wait(payload, player)
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the `onServer` listener on every client (server -> all clients).
|
||||
* One-way: resolves once sent, clients do not answer and their failures stay
|
||||
* on the client.
|
||||
*
|
||||
* @example
|
||||
* await rpc.emitClientEveryone('weatherChanged', 'RAIN')
|
||||
*/
|
||||
public async emitClientEveryone<
|
||||
EventName extends keyof s.RPCEvents_ServerClient,
|
||||
Arguments extends Parameters<s.RPCEvents_ServerClient[EventName]>,
|
||||
>(eventName: EventName, ...args: Arguments): Promise<void> {
|
||||
const payload: RPCState = {
|
||||
event: eventName,
|
||||
uuid: generateUUID(),
|
||||
calledFrom: 'server',
|
||||
calledTo: 'client',
|
||||
error: null,
|
||||
data: args.length ? args : null,
|
||||
player: -1,
|
||||
type: 'event',
|
||||
}
|
||||
EventName extends RPCEventName<s.RPCEvents_ServerClient>,
|
||||
>(
|
||||
eventName: EventName,
|
||||
...args: RPCEventArgs<s.RPCEvents_ServerClient, EventName>
|
||||
): Promise<void> {
|
||||
const payload = this.request(eventName, 'client', args, -1, 'broadcast')
|
||||
|
||||
emitNet(RPCEvents.LISTENER_SERVER, -1, stringify(payload))
|
||||
}
|
||||
|
||||
// ===== WEBVIEW =====
|
||||
|
||||
public onWebview<
|
||||
EventName extends keyof s.RPCEvents_WebviewServer,
|
||||
CallbackArguments extends Parameters<s.RPCEvents_WebviewServer[EventName]>,
|
||||
CallbackReturn extends ReturnType<s.RPCEvents_WebviewServer[EventName]>,
|
||||
>(
|
||||
/**
|
||||
* Listens for `emitServer` calls from webviews (webview -> server, relayed
|
||||
* by the player's client).
|
||||
*
|
||||
* @param cb - gets the calling player's server id first (from FiveM
|
||||
* `source`, never from the payload), then the event arguments. Its return
|
||||
* value (awaited) is sent back to the caller
|
||||
*
|
||||
* @example
|
||||
* rpc.onWebview('buyItem', (player, item) => shop.buy(player, item))
|
||||
*/
|
||||
public onWebview<EventName extends RPCEventName<s.RPCEvents_WebviewServer>>(
|
||||
eventName: EventName,
|
||||
cb: (
|
||||
player: number,
|
||||
...args: CallbackArguments
|
||||
) => Awaited<CallbackReturn> | Promise<Awaited<CallbackReturn>>,
|
||||
cb: RPCListener<s.RPCEvents_WebviewServer, EventName, [player: number]>,
|
||||
): this {
|
||||
if (this.debug) {
|
||||
this.console.log(`[RPC]:onWebview ${eventName}`)
|
||||
return this.listen(this._emitterWeb, 'onWebview', eventName, cb)
|
||||
}
|
||||
|
||||
this._emitterWeb.on(eventName, cb)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public offWebview<EventName extends keyof s.RPCEvents_WebviewServer>(
|
||||
/** Removes the `onWebview` listener for `eventName` */
|
||||
public offWebview<EventName extends RPCEventName<s.RPCEvents_WebviewServer>>(
|
||||
eventName: EventName,
|
||||
): this {
|
||||
if (this.debug) {
|
||||
this.console.log(`[RPC]:offWebview ${eventName}`)
|
||||
}
|
||||
|
||||
this._emitterWeb.off(eventName)
|
||||
|
||||
return this
|
||||
return this.unlisten(this._emitterWeb, 'offWebview', eventName)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the `onServer` listener in one player's webview (server -> webview,
|
||||
* relayed by that player's client) and resolves with its return value.
|
||||
*
|
||||
* @param player - server id of the target player
|
||||
* @throws {@link RPCError} `EVENT_NOT_REGISTERED` (no listener),
|
||||
* `HANDLER_ERROR` (the listener threw) or `TIMEOUT`
|
||||
*
|
||||
* @example
|
||||
* const confirmed = await rpc.emitWebview(player, 'confirmPurchase', item)
|
||||
*/
|
||||
public async emitWebview<
|
||||
EventName extends keyof s.RPCEvents_ServerWebview,
|
||||
Arguments extends Parameters<s.RPCEvents_ServerWebview[EventName]>,
|
||||
Response extends ReturnType<s.RPCEvents_ServerWebview[EventName]>,
|
||||
EventName extends RPCEventName<s.RPCEvents_ServerWebview>,
|
||||
>(
|
||||
player: number,
|
||||
eventName: EventName,
|
||||
...args: Arguments
|
||||
): Promise<Awaited<Response>> {
|
||||
const payload: RPCState = {
|
||||
event: eventName,
|
||||
uuid: generateUUID(),
|
||||
calledFrom: 'server',
|
||||
calledTo: 'webview',
|
||||
error: null,
|
||||
data: args.length ? args : null,
|
||||
player: player,
|
||||
type: 'event',
|
||||
}
|
||||
...args: RPCEventArgs<s.RPCEvents_ServerWebview, EventName>
|
||||
): Promise<RPCEventResult<s.RPCEvents_ServerWebview, EventName>> {
|
||||
const payload = this.request(eventName, 'webview', args, player)
|
||||
|
||||
emitNet(RPCEvents.LISTENER_SERVER, player, stringify(payload))
|
||||
|
||||
return new Promise<Awaited<Response>>(res => {
|
||||
this._pendingWeb.once(payload.uuid, res)
|
||||
})
|
||||
return this._pending.wait(payload, player)
|
||||
}
|
||||
|
||||
// ===== SELF =====
|
||||
|
||||
public onSelf<
|
||||
EventName extends keyof s.RPCEvents_Server,
|
||||
CallbackArguments extends Parameters<s.RPCEvents_Server[EventName]>,
|
||||
CallbackReturn extends ReturnType<s.RPCEvents_Server[EventName]>,
|
||||
>(
|
||||
/**
|
||||
* Listens for `emitSelf` calls in this environment (server -> server).
|
||||
*
|
||||
* @param cb - gets the event arguments. Its return value (awaited) is sent
|
||||
* back to the caller
|
||||
*
|
||||
* @example
|
||||
* rpc.onSelf('add', (a, b) => a + b)
|
||||
*/
|
||||
public onSelf<EventName extends RPCEventName<s.RPCEvents_Server>>(
|
||||
eventName: EventName,
|
||||
cb: (
|
||||
...args: CallbackArguments
|
||||
) => Awaited<CallbackReturn> | Promise<Awaited<CallbackReturn>>,
|
||||
cb: RPCListener<s.RPCEvents_Server, EventName>,
|
||||
): this {
|
||||
if (this.debug) {
|
||||
this.console.log(`[RPC]:onSelf ${eventName}`)
|
||||
return this.listen(this._emitterLocal, 'onSelf', eventName, cb)
|
||||
}
|
||||
|
||||
this._emitterLocal.on(eventName, cb)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public offSelf<EventName extends keyof s.RPCEvents_Server>(
|
||||
/** Removes the `onSelf` listener for `eventName` */
|
||||
public offSelf<EventName extends RPCEventName<s.RPCEvents_Server>>(
|
||||
eventName: EventName,
|
||||
): this {
|
||||
if (this.debug) {
|
||||
this.console.log(`[RPC]:offSelf ${eventName}`)
|
||||
return this.unlisten(this._emitterLocal, 'offSelf', eventName)
|
||||
}
|
||||
|
||||
this._emitterLocal.off(eventName)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public async emitSelf<
|
||||
EventName extends keyof s.RPCEvents_Server,
|
||||
Arguments extends Parameters<s.RPCEvents_Server[EventName]>,
|
||||
Response extends ReturnType<s.RPCEvents_Server[EventName]>,
|
||||
>(eventName: EventName, ...args: Arguments): Promise<Awaited<Response>> {
|
||||
const payload: RPCState = {
|
||||
event: eventName,
|
||||
uuid: generateUUID(),
|
||||
calledFrom: 'server',
|
||||
calledTo: 'server',
|
||||
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 : []),
|
||||
)
|
||||
/**
|
||||
* Calls this environment's own `onSelf` listener directly and resolves with
|
||||
* its return value. No timeout; errors thrown by the listener reach the caller
|
||||
* unchanged.
|
||||
*
|
||||
* @throws {@link RPCError} `EVENT_NOT_REGISTERED` if no `onSelf` listener exists
|
||||
*
|
||||
* @example
|
||||
* const total = await rpc.emitSelf('add', 2, 3)
|
||||
*/
|
||||
public async emitSelf<EventName extends RPCEventName<s.RPCEvents_Server>>(
|
||||
eventName: EventName,
|
||||
...args: RPCEventArgs<s.RPCEvents_Server, EventName>
|
||||
): Promise<RPCEventResult<s.RPCEvents_Server, EventName>> {
|
||||
return this.emitLocal(eventName, args)
|
||||
}
|
||||
|
||||
// ===== OTHER =====
|
||||
|
||||
public onCommand<
|
||||
CommandName extends s.RPCCommands_Server,
|
||||
CallbackArguments extends unknown[],
|
||||
>(
|
||||
/**
|
||||
* Registers a chat command (FiveM `RegisterCommand`).
|
||||
*
|
||||
* @param cb - gets the player's server id (`0` for the server console), the
|
||||
* strings typed after the command and the full command line. Validate
|
||||
* `args` yourself
|
||||
* @param restricted - only players with the ACE permission `command.<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,
|
||||
cb: (player: number, args: CallbackArguments, commandRaw: string) => void,
|
||||
cb: (player: number, args: string[], rawCommand: string) => void,
|
||||
restricted = false,
|
||||
): this {
|
||||
if (this.debug) {
|
||||
this.console.log(`[RPC]:onCommand ${command}`)
|
||||
}
|
||||
this.log(`onCommand ${command}`)
|
||||
|
||||
RegisterCommand(command, cb, restricted)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public onNativeEvent<
|
||||
EventName extends keyof RPCNativeServerEvents,
|
||||
CallbackArguments extends Parameters<RPCNativeServerEvents[EventName]>,
|
||||
>(eventName: EventName, cb: (...args: CallbackArguments) => void): this {
|
||||
/**
|
||||
* Listens to a native FiveM server event, e.g. `playerJoining`.
|
||||
*
|
||||
* @throws {@link RPCError} `UNKNOWN_NATIVE` if `eventName` is not in
|
||||
* `NATIVE_SERVER_EVENTS`. Register other events with FiveM's `on` directly
|
||||
*
|
||||
* @example
|
||||
* rpc.onNativeEvent('playerJoining', (source, oldId) => console.log(source))
|
||||
*/
|
||||
public onNativeEvent<EventName extends keyof RPCNativeServerEvents>(
|
||||
eventName: EventName,
|
||||
cb: (...args: Parameters<RPCNativeServerEvents[EventName]>) => void,
|
||||
): this {
|
||||
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.console.log(`[RPC]:onNativeEvent ${eventName}`)
|
||||
}
|
||||
this.log(`onNativeEvent ${eventName}`)
|
||||
|
||||
on(eventName, cb)
|
||||
|
||||
|
||||
+172
-211
@@ -1,22 +1,34 @@
|
||||
import type * as s from '@entityseven/fivem-rpc-shared-types'
|
||||
|
||||
import { Emitter } from '../utils/emitter'
|
||||
import { generateUUID, stringify } from '../utils/funcs'
|
||||
import { stringify } from '../utils/funcs'
|
||||
import {
|
||||
RPCEvents,
|
||||
type RPCConfig,
|
||||
type RPCState,
|
||||
type RPCStateRaw,
|
||||
type RPCStateWeb,
|
||||
} from '../utils/types'
|
||||
import { Wrapper } from './wrapper'
|
||||
import type {
|
||||
RPCEventArgs,
|
||||
RPCEventName,
|
||||
RPCEventResult,
|
||||
RPCListener,
|
||||
} from '../utils/typing'
|
||||
import { RPCInstanceBase } from './base'
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
GetParentResourceName?: () => string
|
||||
}
|
||||
}
|
||||
|
||||
export class RPCInstanceWebview extends Wrapper {
|
||||
/**
|
||||
* RPC instance for webview code, returned by `createRPC({ env: 'webview' })`.
|
||||
* Create one per webview and import it from your own module.
|
||||
*
|
||||
* - `on*` registers the listener that answers calls from one direction. One
|
||||
* listener per event: registering the same name again replaces it, `off*`
|
||||
* removes it
|
||||
* - `emit*` calls the listener on the target and resolves with its return
|
||||
* value, or rejects with {@link RPCError}
|
||||
* - calls to and from the server are relayed by the player's client, which
|
||||
* must run `createRPC({ env: 'client' })`
|
||||
*/
|
||||
export class RPCInstanceWebview extends RPCInstanceBase {
|
||||
private readonly _emitterClient: Emitter
|
||||
private readonly _emitterServer: Emitter
|
||||
|
||||
@@ -26,75 +38,42 @@ export class RPCInstanceWebview extends Wrapper {
|
||||
this._emitterClient = new Emitter()
|
||||
this._emitterServer = new Emitter()
|
||||
|
||||
this.console.log('[RPC] Initialized Webview')
|
||||
console.log('[RPC] Initialized Webview')
|
||||
|
||||
window.addEventListener('message', (e: MessageEvent<RPCStateWeb>) => {
|
||||
if (e.data.origin === RPCEvents.LISTENER_CLIENT) {
|
||||
this._handleClient(e.data.data)
|
||||
window.addEventListener(
|
||||
'message',
|
||||
(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 =====
|
||||
|
||||
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') {
|
||||
this.verifyEvent(this._emitterClient, payload)
|
||||
const response = await this.dispatch(this._emitterClient, payload)
|
||||
|
||||
const responseData = await this._emitterClient.emit(
|
||||
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()
|
||||
await this._createHttpClientRequest(response)
|
||||
}
|
||||
}
|
||||
|
||||
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') {
|
||||
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: 'webview',
|
||||
calledTo: 'server',
|
||||
error: null,
|
||||
data: [responseData],
|
||||
player: payload.player,
|
||||
type: 'response',
|
||||
}
|
||||
const response = await this.dispatch(this._emitterServer, payload)
|
||||
|
||||
await this._createHttpClientRequest(response)
|
||||
}
|
||||
@@ -102,184 +81,166 @@ export class RPCInstanceWebview extends Wrapper {
|
||||
|
||||
// ===== CLIENT =====
|
||||
|
||||
public onClient<
|
||||
EventName extends keyof s.RPCEvents_ClientWebview,
|
||||
CallbackArguments extends Parameters<s.RPCEvents_ClientWebview[EventName]>,
|
||||
CallbackReturn extends ReturnType<s.RPCEvents_ClientWebview[EventName]>,
|
||||
/**
|
||||
* Listens for `emitWebview` calls from this player's client
|
||||
* (client -> webview).
|
||||
*
|
||||
* @param cb - gets the event arguments. Its return value (awaited) is sent
|
||||
* back to the caller
|
||||
*
|
||||
* @example
|
||||
* rpc.onClient('openMenu', items => menu.open(items))
|
||||
*/
|
||||
public onClient<EventName 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,
|
||||
cb: (
|
||||
...args: CallbackArguments
|
||||
) => Awaited<CallbackReturn> | Promise<Awaited<CallbackReturn>>,
|
||||
): this {
|
||||
if (this.debug) {
|
||||
this.console.log(`[RPC]:onClient ${eventName}`)
|
||||
}
|
||||
...args: RPCEventArgs<s.RPCEvents_WebviewClient, EventName>
|
||||
): Promise<RPCEventResult<s.RPCEvents_WebviewClient, EventName>> {
|
||||
const payload = this.request(eventName, 'client', args, null)
|
||||
|
||||
this._emitterClient.on(eventName, cb)
|
||||
|
||||
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)
|
||||
return this._request(payload)
|
||||
}
|
||||
|
||||
// ===== SERVER =====
|
||||
|
||||
public onServer<
|
||||
EventName extends keyof s.RPCEvents_ServerWebview,
|
||||
CallbackArguments extends Parameters<s.RPCEvents_ServerWebview[EventName]>,
|
||||
CallbackReturn extends ReturnType<s.RPCEvents_ServerWebview[EventName]>,
|
||||
/**
|
||||
* Listens for `emitWebview` calls from the server (server -> webview,
|
||||
* relayed by the client).
|
||||
*
|
||||
* @param cb - gets the event arguments. Its return value (awaited) is sent
|
||||
* back to the caller
|
||||
*
|
||||
* @example
|
||||
* rpc.onServer('confirmPurchase', item => window.confirm(`Buy ${item}?`))
|
||||
*/
|
||||
public onServer<EventName 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,
|
||||
cb: (
|
||||
...args: CallbackArguments
|
||||
) => Awaited<CallbackReturn> | Promise<Awaited<CallbackReturn>>,
|
||||
): this {
|
||||
if (this.debug) {
|
||||
this.console.log(`[RPC]:onServer ${eventName}`)
|
||||
}
|
||||
...args: RPCEventArgs<s.RPCEvents_WebviewServer, EventName>
|
||||
): Promise<RPCEventResult<s.RPCEvents_WebviewServer, EventName>> {
|
||||
const payload = this.request(eventName, 'server', args, null)
|
||||
|
||||
this._emitterServer.on(eventName, cb)
|
||||
|
||||
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)
|
||||
return this._request(payload)
|
||||
}
|
||||
|
||||
// ===== SELF =====
|
||||
|
||||
public onSelf<
|
||||
EventName extends keyof s.RPCEvents_Webview,
|
||||
CallbackArguments extends Parameters<s.RPCEvents_Webview[EventName]>,
|
||||
CallbackReturn extends ReturnType<s.RPCEvents_Webview[EventName]>,
|
||||
>(
|
||||
/**
|
||||
* Listens for `emitSelf` calls in this environment (webview -> webview).
|
||||
*
|
||||
* @param cb - gets the event arguments. Its return value (awaited) is sent
|
||||
* back to the caller
|
||||
*
|
||||
* @example
|
||||
* rpc.onSelf('add', (a, b) => a + b)
|
||||
*/
|
||||
public onSelf<EventName extends RPCEventName<s.RPCEvents_Webview>>(
|
||||
eventName: EventName,
|
||||
cb: (
|
||||
...args: CallbackArguments
|
||||
) => Awaited<CallbackReturn> | Promise<Awaited<CallbackReturn>>,
|
||||
cb: RPCListener<s.RPCEvents_Webview, EventName>,
|
||||
): this {
|
||||
if (this.debug) {
|
||||
this.console.log(`[RPC]:onSelf ${eventName}`)
|
||||
return this.listen(this._emitterLocal, 'onSelf', eventName, cb)
|
||||
}
|
||||
|
||||
this._emitterLocal.on(eventName, cb)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public offSelf<EventName extends keyof s.RPCEvents_Webview>(
|
||||
/** Removes the `onSelf` listener for `eventName` */
|
||||
public offSelf<EventName extends RPCEventName<s.RPCEvents_Webview>>(
|
||||
eventName: EventName,
|
||||
): this {
|
||||
if (this.debug) {
|
||||
this.console.log(`[RPC]:offSelf ${eventName}`)
|
||||
return this.unlisten(this._emitterLocal, 'offSelf', eventName)
|
||||
}
|
||||
|
||||
this._emitterLocal.off(eventName)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public async emitSelf<
|
||||
EventName extends keyof s.RPCEvents_Webview,
|
||||
Arguments extends Parameters<s.RPCEvents_Webview[EventName]>,
|
||||
Response extends ReturnType<s.RPCEvents_Webview[EventName]>,
|
||||
>(eventName: EventName, ...args: Arguments): Promise<Awaited<Response>> {
|
||||
const payload: RPCState = {
|
||||
event: eventName,
|
||||
uuid: generateUUID(),
|
||||
calledFrom: 'webview',
|
||||
calledTo: 'webview',
|
||||
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 : []),
|
||||
)
|
||||
/**
|
||||
* Calls this environment's own `onSelf` listener directly and resolves with
|
||||
* its return value. No timeout; errors thrown by the listener reach the caller
|
||||
* unchanged.
|
||||
*
|
||||
* @throws {@link RPCError} `EVENT_NOT_REGISTERED` if no `onSelf` listener exists
|
||||
*
|
||||
* @example
|
||||
* const total = await rpc.emitSelf('add', 2, 3)
|
||||
*/
|
||||
public async emitSelf<EventName extends RPCEventName<s.RPCEvents_Webview>>(
|
||||
eventName: EventName,
|
||||
...args: RPCEventArgs<s.RPCEvents_Webview, EventName>
|
||||
): Promise<RPCEventResult<s.RPCEvents_Webview, EventName>> {
|
||||
return this.emitLocal(eventName, args)
|
||||
}
|
||||
|
||||
// ===== UTILS =====
|
||||
|
||||
private async _createHttpClientRequest<R>(
|
||||
data: RPCStateRaw | RPCState,
|
||||
): Promise<R> {
|
||||
const dataRaw = typeof data === 'string' ? data : stringify(data)
|
||||
/** Sends an event to the client and waits for its response (with timeout) */
|
||||
private _request<R>(payload: RPCState): Promise<R> {
|
||||
const response = this._pending.wait<R>(payload)
|
||||
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 = {
|
||||
method: 'post',
|
||||
headers: {
|
||||
'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'
|
||||
return fetch(
|
||||
`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 { RPCInstanceServer } from './core/server'
|
||||
import { RPCInstanceWebview } from './core/webview'
|
||||
import { Wrapper } from './core/wrapper'
|
||||
import { RPCError, unknownEnvironmentMessage } from './utils/errors'
|
||||
import {
|
||||
type RPCConfig,
|
||||
type RPCEnvironment,
|
||||
@@ -10,65 +10,60 @@ import {
|
||||
} 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
|
||||
* // returns RPCInstanceServer
|
||||
* const rpc = new RPCFactory({ env: "server" }).get()
|
||||
*
|
||||
* @example
|
||||
* // returns RPCInstanceClient
|
||||
* const rpc = new RPCFactory({ env: "client" }).get()
|
||||
*
|
||||
* @example
|
||||
* // returns RPCInstanceWebview
|
||||
* const rpc = new RPCFactory({ env: "webview" }).get()
|
||||
*
|
||||
* @class
|
||||
* // server/rpc.ts
|
||||
* import { createRPC } from '@entityseven/fivem-rpc'
|
||||
* export const rpc = createRPC({ env: 'server' })
|
||||
*/
|
||||
class RPCFactory<T extends RPCEnvironment> extends Wrapper {
|
||||
private readonly operator:
|
||||
| RPCInstanceServer
|
||||
| RPCInstanceClient
|
||||
| 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) {
|
||||
export function createRPC<T extends RPCEnvironment>(
|
||||
config: RPCConfig<T>,
|
||||
): RPCEnvironmentResolved<T> {
|
||||
switch (config.env) {
|
||||
case 'server':
|
||||
this.operator = new RPCInstanceServer(opts as RPCConfig<'server'>)
|
||||
break
|
||||
return new RPCInstanceServer(
|
||||
config as RPCConfig<'server'>,
|
||||
) as RPCEnvironmentResolved<T>
|
||||
case 'client':
|
||||
this.operator = new RPCInstanceClient(opts as RPCConfig<'client'>)
|
||||
break
|
||||
return new RPCInstanceClient(
|
||||
config as RPCConfig<'client'>,
|
||||
) as RPCEnvironmentResolved<T>
|
||||
case 'webview':
|
||||
this.operator = new RPCInstanceWebview(opts as RPCConfig<'webview'>)
|
||||
break
|
||||
return new RPCInstanceWebview(
|
||||
config as RPCConfig<'webview'>,
|
||||
) as RPCEnvironmentResolved<T>
|
||||
default:
|
||||
throw new Error(RPCErrors.UNKNOWN_ENVIRONMENT)
|
||||
throw new RPCError(
|
||||
RPCErrors.UNKNOWN_ENVIRONMENT,
|
||||
unknownEnvironmentMessage(config.env),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public get(): RPCEnvironmentResolved<T> {
|
||||
return this.operator as RPCEnvironmentResolved<T>
|
||||
}
|
||||
}
|
||||
|
||||
export { RPCFactory }
|
||||
// export const rpcClient = new RPCFactory({ env: "client" }).get();
|
||||
// export const rpcServer = new RPCFactory({ env: "server" }).get();
|
||||
// export const rpcWebview = new RPCFactory({ env: "webview" }).get();
|
||||
export * from './utils/types'
|
||||
export * from './utils/native'
|
||||
export type * from './core/server'
|
||||
export type * from './core/client'
|
||||
export type * from './core/webview'
|
||||
export type { RPCInstanceClient } from './core/client'
|
||||
export type { RPCInstanceServer } from './core/server'
|
||||
export type { RPCInstanceWebview } from './core/webview'
|
||||
export { RPCError, type RPCErrorDetails } from './utils/errors'
|
||||
export {
|
||||
NATIVE_CLIENT_EVENTS,
|
||||
NATIVE_CLIENT_NETWORK_EVENTS,
|
||||
NATIVE_SERVER_EVENTS,
|
||||
} from './utils/native'
|
||||
export {
|
||||
type RPCConfig,
|
||||
type RPCEnvironment,
|
||||
RPCErrors,
|
||||
type RPCNativeClientEvents,
|
||||
type RPCNativeClientNetworkEvents,
|
||||
type RPCNativeClientNetworkEventsNames,
|
||||
type RPCNativeServerEvents,
|
||||
} from './utils/types'
|
||||
|
||||
+17
-25
@@ -1,23 +1,22 @@
|
||||
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 {
|
||||
/** Map<event, [callback function, once]> */
|
||||
private _storage: Map<string, [(...args: any[]) => any, boolean]>
|
||||
/** Map<event, [handler, once]> */
|
||||
private _storage = new Map<string, [Handler, boolean]>()
|
||||
|
||||
constructor() {
|
||||
this._storage = new Map()
|
||||
}
|
||||
|
||||
get _raw_storage() {
|
||||
return this._storage
|
||||
}
|
||||
|
||||
public on(event: string, cb: (...args: any[]) => any): this {
|
||||
public on(event: string, cb: Handler): this {
|
||||
this._storage.set(event, [cb, false])
|
||||
return this
|
||||
}
|
||||
|
||||
public once(event: string, cb: (...args: any[]) => any): this {
|
||||
public once(event: string, cb: Handler): this {
|
||||
this._storage.set(event, [cb, true])
|
||||
return this
|
||||
}
|
||||
@@ -31,24 +30,17 @@ export class Emitter {
|
||||
return this._storage.has(event)
|
||||
}
|
||||
|
||||
public async emit<R>(event: string, ...args: any[]): Promise<R> {
|
||||
return new Promise((res, rej) => {
|
||||
if (!this._storage.has(event)) {
|
||||
rej(RPCErrors.EVENT_NOT_REGISTERED)
|
||||
public async emit<R>(event: string, ...args: unknown[]): Promise<R> {
|
||||
const entry = this._storage.get(event)
|
||||
if (!entry) {
|
||||
throw new Error(RPCErrors.EVENT_NOT_REGISTERED)
|
||||
}
|
||||
|
||||
const [cb, once] = this._storage.get(event) as [
|
||||
(...args: any[]) => any,
|
||||
boolean,
|
||||
]
|
||||
|
||||
const [cb, once] = entry
|
||||
if (once) {
|
||||
this._storage.delete(event)
|
||||
}
|
||||
|
||||
Promise.resolve(cb(...args))
|
||||
.then(res)
|
||||
.catch(rej)
|
||||
})
|
||||
return (await cb(...(args as never[]))) as R
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
RPCEnvironment,
|
||||
RPCState,
|
||||
RPCStateRaw,
|
||||
RPCStateWeb,
|
||||
RPCStateWebRaw,
|
||||
} from './types'
|
||||
|
||||
const ENVIRONMENTS: readonly unknown[] = [
|
||||
'server',
|
||||
'client',
|
||||
'webview',
|
||||
] satisfies RPCEnvironment[]
|
||||
|
||||
/**
|
||||
* **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 {
|
||||
return JSON.parse(data)
|
||||
export function isRPCState(value: unknown): value is RPCState {
|
||||
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
|
||||
}
|
||||
|
||||
// automatically parsed by FiveM
|
||||
// export function parseWeb(data: RPCStateWebRaw): RPCStateWeb {
|
||||
// return JSON.parse(data)
|
||||
// }
|
||||
|
||||
/**
|
||||
* **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 {
|
||||
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 {
|
||||
let uuid = ''
|
||||
let random = 0
|
||||
|
||||
+49
-41
@@ -1,56 +1,64 @@
|
||||
import type {
|
||||
RPCNativeClientEvents,
|
||||
RPCNativeClientNetworkEventsNames,
|
||||
RPCNativeServerEvents,
|
||||
} from './types'
|
||||
import type { RPCNativeClientEvents, RPCNativeServerEvents } from './types'
|
||||
|
||||
// Server and client lists are built from an object that must name exactly the
|
||||
// keys of the typed event map, so the list and the types cannot drift apart.
|
||||
|
||||
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/
|
||||
* @readonly
|
||||
*/
|
||||
export const NATIVE_SERVER_EVENTS: readonly (keyof RPCNativeServerEvents)[] = [
|
||||
'entityCreated',
|
||||
'entityCreating',
|
||||
'entityRemoved',
|
||||
'onResourceListRefresh',
|
||||
'onResourceStart',
|
||||
'onResourceStarting',
|
||||
'onResourceStop',
|
||||
'onServerResourceStart',
|
||||
'onServerResourceStop',
|
||||
'playerConnecting',
|
||||
'playerEnteredScope',
|
||||
'playerJoining',
|
||||
'playerLeftScope',
|
||||
'ptFxEvent',
|
||||
'removeAllWeaponsEvent',
|
||||
'startProjectileEvent',
|
||||
'weaponDamageEvent',
|
||||
] as const
|
||||
export const NATIVE_SERVER_EVENTS = Object.keys(
|
||||
serverEvents,
|
||||
) as readonly (keyof RPCNativeServerEvents)[]
|
||||
|
||||
const clientEvents = {
|
||||
entityDamaged: true,
|
||||
gameEventTriggered: true,
|
||||
mumbleConnected: true,
|
||||
mumbleDisconnected: true,
|
||||
onClientResourceStart: true,
|
||||
onClientResourceStop: true,
|
||||
onResourceStart: true,
|
||||
onResourceStarting: true,
|
||||
onResourceStop: true,
|
||||
populationPedCreating: true,
|
||||
} satisfies Record<keyof RPCNativeClientEvents, true>
|
||||
|
||||
/**
|
||||
* Events `onNativeEvent` accepts on the client:
|
||||
* https://docs.fivem.net/docs/scripting-reference/events/client-events/
|
||||
* @readonly
|
||||
*/
|
||||
export const NATIVE_CLIENT_EVENTS: readonly (keyof RPCNativeClientEvents)[] = [
|
||||
'entityDamaged',
|
||||
'gameEventTriggered',
|
||||
'mumbleConnected',
|
||||
'mumbleDisconnected',
|
||||
'onClientResourceStart',
|
||||
'onClientResourceStop',
|
||||
'onResourceStart',
|
||||
'onResourceStarting',
|
||||
'onResourceStop',
|
||||
'populationPedCreating',
|
||||
] as const
|
||||
export const NATIVE_CLIENT_EVENTS = Object.keys(
|
||||
clientEvents,
|
||||
) as readonly (keyof RPCNativeClientEvents)[]
|
||||
|
||||
/**
|
||||
* Events `onNativeNetworkEvent` accepts on the client:
|
||||
* https://docs.fivem.net/docs/game-references/game-events/
|
||||
* @readonly
|
||||
*
|
||||
* Source of truth for `RPCNativeClientNetworkEventsNames`.
|
||||
*/
|
||||
export const NATIVE_CLIENT_NETWORK_EVENTS: readonly RPCNativeClientNetworkEventsNames[] =
|
||||
[
|
||||
export const NATIVE_CLIENT_NETWORK_EVENTS = [
|
||||
'CEventAcquaintancePed',
|
||||
'CEventAcquaintancePedDead',
|
||||
'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 { RPCInstanceServer } from '../core/server'
|
||||
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'
|
||||
|
||||
@@ -16,18 +18,33 @@ export type RPCEnvironmentResolved<T extends RPCEnvironment> =
|
||||
? RPCInstanceWebview
|
||||
: never
|
||||
|
||||
/**
|
||||
* `RPCFactory` config.
|
||||
*
|
||||
* If environment does not match will throw `RPCErrors.UNKNOWN_ENVIRONMENT`
|
||||
*/
|
||||
export type RPCConfig<T extends RPCEnvironment | unknown> = {
|
||||
/** `createRPC` config */
|
||||
export type RPCConfig<T extends RPCEnvironment = RPCEnvironment> = {
|
||||
/** Environment this instance runs in */
|
||||
env: T
|
||||
/**
|
||||
* Log every registration, call and incoming payload
|
||||
*
|
||||
* @defaultValue false
|
||||
*/
|
||||
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**
|
||||
@@ -39,12 +56,19 @@ export type RPCState = {
|
||||
uuid: string
|
||||
calledFrom: RPCEnvironment
|
||||
calledTo: RPCEnvironment
|
||||
error: string | null
|
||||
error: RPCErrorPayload | null
|
||||
data: unknown[] | null
|
||||
/** Server id of the player involved. The server fills it from `source`, never trusting the sender */
|
||||
player: number | null
|
||||
type: RPCEventType
|
||||
}
|
||||
|
||||
/** **Internal** Error sent back in a response, rebuilt as `RPCError` by the caller */
|
||||
export type RPCErrorPayload = {
|
||||
code: RPCErrors
|
||||
message: string
|
||||
}
|
||||
|
||||
/**
|
||||
* **Internal**
|
||||
*
|
||||
@@ -76,18 +100,22 @@ export enum RPCEvents {
|
||||
LISTENER_WEB = '__rpc:listenerWeb',
|
||||
}
|
||||
|
||||
/**
|
||||
* Errors to check against
|
||||
*/
|
||||
/** Values of `RPCError.code` */
|
||||
export enum RPCErrors {
|
||||
/** The target has no listener for the event (or `emitSelf` has no `onSelf`) */
|
||||
EVENT_NOT_REGISTERED = 'Event not registered',
|
||||
INVALID_DATA = 'Invalid data (possibly broken JSON)',
|
||||
NO_PLAYER = 'No player (failed to resolve from local index)',
|
||||
UNKNOWN_NATIVE = 'Unknown native event (if you are sure this exists - use native handler)',
|
||||
UNKNOWN_ENVIRONMENT = 'Unknown environment (must be either "server", "client" or "webview")',
|
||||
/** `onNative*` got an event that is not in its `NATIVE_*` list */
|
||||
UNKNOWN_NATIVE = 'Unknown native event',
|
||||
/** `createRPC` got an `env` other than server, client or webview */
|
||||
UNKNOWN_ENVIRONMENT = 'Unknown environment',
|
||||
/** No response within `RPCConfig.timeout` */
|
||||
TIMEOUT = 'Timed out waiting for response',
|
||||
/** The listener on the target threw; the message carries its error */
|
||||
HANDLER_ERROR = 'Listener threw an error',
|
||||
}
|
||||
|
||||
/**
|
||||
* Native server events accepted by `onNativeEvent` on the server:
|
||||
* https://docs.fivem.net/docs/scripting-reference/events/server-events/
|
||||
*/
|
||||
export type RPCNativeServerEvents = {
|
||||
@@ -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/
|
||||
*/
|
||||
export type RPCNativeClientEvents = {
|
||||
@@ -232,7 +261,7 @@ export type RPCNativeClientEvents = {
|
||||
baseDamage: number,
|
||||
): void
|
||||
gameEventTriggered(
|
||||
name: RPCNativeClientNetworksEvents | string,
|
||||
name: RPCNativeClientNetworkEventsNames | (string & {}),
|
||||
data: number[],
|
||||
): void
|
||||
mumbleConnected(address: string, reconnecting: boolean): void
|
||||
@@ -254,7 +283,8 @@ export type RPCNativeClientEvents = {
|
||||
): void
|
||||
}
|
||||
|
||||
export type RPCNativeClientNetworksEvents = {
|
||||
/** Game events accepted by `onNativeNetworkEvent` on the client */
|
||||
export type RPCNativeClientNetworkEvents = {
|
||||
[name in RPCNativeClientNetworkEventsNames]: (
|
||||
entities: 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/
|
||||
*/
|
||||
export type RPCNativeClientNetworkEventsNames =
|
||||
| 'CEventAcquaintancePed'
|
||||
| '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'
|
||||
(typeof NATIVE_CLIENT_NETWORK_EVENTS)[number]
|
||||
|
||||
@@ -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": {
|
||||
"target": "es6",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"lib": ["ES6", "dom"],
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"target": "es2022",
|
||||
"lib": ["es2022", "dom"],
|
||||
"module": "preserve",
|
||||
"moduleResolution": "bundler",
|
||||
|
||||
"outDir": "dist",
|
||||
"esModuleInterop": true,
|
||||
"noEmit": true,
|
||||
"isolatedModules": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noImplicitAny": true
|
||||
"strict": 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",
|
||||
"description": "Shared types for @entityseven/fivem-rpc. Highly recommended to install together",
|
||||
"version": "0.1.0",
|
||||
"types": "types/types/index.d.ts",
|
||||
"files": [
|
||||
"types/**/*",
|
||||
"readme.md",
|
||||
"license.md"
|
||||
],
|
||||
"version": "1.0.0",
|
||||
"description": "Event and command declarations for @entityseven/fivem-rpc, for typed events",
|
||||
"keywords": [
|
||||
"fivem-rpc-shared-types",
|
||||
"fivem-rpc",
|
||||
"cfx",
|
||||
"fivem",
|
||||
"gta"
|
||||
"fivem-rpc",
|
||||
"types",
|
||||
"typescript"
|
||||
],
|
||||
"type": "module",
|
||||
"license": "SEE LICENSE IN license.md",
|
||||
"author": "Entity Seven Group",
|
||||
"contributors": [
|
||||
{
|
||||
"name": "Danya H",
|
||||
"email": "[email protected]",
|
||||
"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": {
|
||||
"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