Merge pull request #1 from rilaxik/v1

v1
This commit is contained in:
2026-09-28 02:27:24 +01:00
committed by GitHub
34 changed files with 2575 additions and 1918 deletions
+13
View File
@@ -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
+2
View File
@@ -0,0 +1,2 @@
* text=auto eol=crlf
bun.lock text eol=lf
-1
View File
@@ -2,4 +2,3 @@
.vscode
dist
node_modules
bun.lock
+20
View File
@@ -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" }
}
]
}
+22
View File
@@ -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"]
}
-47
View File
@@ -1,47 +0,0 @@
{
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
"vcs": {
"enabled": false,
"clientKind": "git",
"useIgnoreFile": false
},
"files": {
"ignoreUnknown": false,
"includes": ["**", "!dist/*"]
},
"formatter": {
"enabled": true
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"complexity": {
"noBannedTypes": "info"
},
"suspicious": {
"noExplicitAny": "info"
}
}
},
"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"
}
}
}
}
+302
View File
@@ -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=="],
}
}
+53
View File
@@ -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
View File
@@ -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"
}
}
+68 -22
View File
@@ -1,41 +1,87 @@
# FiveM RPC
is an all-in-one package with asynchronous RPC implementation for FiveM servers in JS/TS
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
pnpm i @entityseven/fivem-rpc
```
```bash
yarn add @entityseven/fivem-rpc
```
```bash
bun add @entityseven/fivem-rpc
```
It is highly recommended to also install additional package for enhanced typing
```bash
pnpm i @entityseven/fivem-rpc-shared-types -D
```
```bash
yarn add @entityseven/fivem-rpc-shared-types --dev
```
```bash
bun add @entityseven/fivem-rpc-shared-types -d
npm i @entityseven/fivem-rpc
pnpm add @entityseven/fivem-rpc
yarn add @entityseven/fivem-rpc
bun add @entityseven/fivem-rpc
```
## Docs
Can be found in [/rpc/readme.md](https://github.com/rilaxik/fivem-rpc/blob/master/rpc/readme.md)
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 Custom Attribution-NoDerivs Software License
Licensed under the [Custom Attribution-NoDerivs Software License](license.md)
## Roadmap
## WIP
- 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)
+46 -20
View File
@@ -1,43 +1,69 @@
{
"name": "@entityseven/fivem-rpc",
"description": "FiveM RPC is an all-in-one package with asynchronous RPC implementation for FiveM servers in JS/TS",
"version": "0.1.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist/**/*",
"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"
],
"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"
"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": "tsup"
"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
}
}
}
+205 -69
View File
@@ -1,300 +1,436 @@
# FiveM RPC
is an all-in package with asynchronous RPC implementation for RageMP servers in JS/TS. [Extra info](https://github.com/rilaxik/fivem-rpc/blob/master/readme.md)
# 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)
Call FiveM server, client and NUI listeners like async functions: typed, with timeouts, no event ping-pong
# Installation
```bash
pnpm i @entityseven/fivem-rpc
```
```bash
yarn add @entityseven/fivem-rpc
```
```bash
bun add @entityseven/fivem-rpc
```
It is highly recommended to also install additional package for enhanced typing
```bash
pnpm i @entityseven/fivem-rpc-shared-types -D
```
```bash
yarn add @entityseven/fivem-rpc-shared-types --dev
```
```bash
bun add @entityseven/fivem-rpc-shared-types -d
```
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).
## Usage
FiveM RPC is meant to be a singletone per environment. This means you _must create only one_ `RPCFactory` per your server/client/web. This also enables modifying `const rpc` to your needs, adding new methods or variables by forcing you to import it from file instead of library reference
```ts
// lib/rpc.ts
import { RPCFactory } from '@entityseven/fivem-rpc'
export const rpc = new RPCFactory(/* options */).get()
```
## Exports
# Docs
Besides `createRPC` the package exports:
## Extras
Along with `RPCFactory` you can also import all the types used internally, types for native client/server events and lists of native client/server events. All of that is documented in JSDoc, so no need to duplicate it here
- `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<T extends RPCEnvironment | unknown> = {
env: T
debug?: boolean
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
}
```
Failing to set `env` to provided type will result in `RPCErrors.UNKNOWN_ENVIRONMENT`
`debug` adds additional console logs to events
An unknown `env` makes `createRPC` throw `RPCError` with code `RPCErrors.UNKNOWN_ENVIRONMENT`
## Errors
Known errors could be one of following or an error throw by a callback specifically
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',
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")',
UNKNOWN_NATIVE = 'Unknown native event',
UNKNOWN_ENVIRONMENT = 'Unknown environment',
TIMEOUT = 'Timed out waiting for response',
HANDLER_ERROR = 'Listener threw an error',
}
```
### Example error
Values wrapped in `<>` always exist, just not relevant for an example. Keep in mind that some errors are thrown in their destination(`To`) point: this example will throw on server
```
Error: No player (failed to resolve from local index)
Event: 'clientServerEvent'
Uuid: <uuid>
From: 'client'
To: 'server'
Player: <non-existent-player>
Type: 'event'
Data: [<data>]
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' }
}
}
```
## Server ([source](https://github.com/rilaxik/fivem-rpc/blob/master/rpc/src/core/server.ts))
## 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
Sends event to all clients. One-way: clients run their listener but do not answer
```ts
rpc.emitClientEveryone('serverClientEvent', someData)
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 specified webview
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. Since arguments are untyped you must validate them yourself
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, commandRaw) => {
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](https://github.com/rilaxik/fivem-rpc/blob/master/rpc/src/core/client.ts))
## 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 webview listener with returned data
// 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 specified webview
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. Since arguments are untyped you must validate them yourself
Registers chat command. `args` are the raw strings typed after the command, validate them yourself
```ts
rpc.onCommand('clientCommand', (player, args, commandRaw) => {
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](https://github.com/rilaxik/fivem-rpc/blob/master/rpc/src/core/webview.ts))
## 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 webview listener with returned data
// 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
```
# License
Licensed under Custom Attribution-NoDerivs Software License
## 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)
+105
View File
@@ -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
+208
View File
@@ -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 -258
View File
@@ -1,6 +1,8 @@
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,
@@ -10,52 +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'
declare function onNet(eventName: string, callback: Function): void
declare function emitNet(eventName: string, ...args: unknown[]): void
declare function on(eventName: string, callback: Function): void
declare function RegisterNuiCallbackType(callbackType: string): void
declare function GetPlayerServerId(player: number): number
declare function PlayerId(): number
declare function SetNuiFocus(hasFocus: boolean, hasCursor: boolean): void
declare function RegisterCommand(
commandName: string,
handler: Function,
restricted: boolean,
): void
declare function SendNuiMessage(jsonString: string): boolean
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))
}
},
)
}
@@ -63,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') {
if (payload.type === 'event' || payload.type === 'broadcast') {
if (payload.calledTo === 'client') {
this.verifyEvent(this._emitterServer, payload)
const response = await this.dispatch(this._emitterServer, payload)
const responseData = await this._emitterServer.emit(
payload.event,
...(payload.data && payload.data.length > 0 ? payload.data : []),
)
const response: RPCState = {
event: payload.event,
uuid: payload.uuid,
calledFrom: 'client',
calledTo: 'server',
error: null,
data: [responseData],
player: payload.player,
type: 'response',
if (payload.type === 'event') {
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}`,
)
}
emitNet(RPCEvents.LISTENER_CLIENT, stringify(response))
}
if (payload.calledTo === 'webview') {
this._sendWebMessage({
@@ -107,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' }
@@ -166,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}`)
}
this._emitterLocal.on(eventName, cb)
return this
return this.listen(this._emitterLocal, 'onSelf', eventName, cb)
}
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}`)
}
this._emitterLocal.off(eventName)
return this
return this.unlisten(this._emitterLocal, 'offSelf', eventName)
}
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)
+199 -281
View File
@@ -1,383 +1,301 @@
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'
declare function onNet(eventName: string, callback: Function): void
declare function emitNet(eventName: string, ...args: unknown[]): void
declare function on(eventName: string, callback: Function): void
declare function RegisterCommand(
commandName: string,
handler: Function,
restricted: boolean,
): void
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)
/**
* 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
// 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
}
const payload = parse(payloadRaw)
payload.player = player
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)
return
}
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 : []),
)
}
if (payload.type === 'event') {
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}`)
}
this._emitterClient.on(eventName, cb)
return this
return this.listen(this._emitterClient, 'onClient', eventName, cb)
}
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}`)
}
this._emitterWeb.on(eventName, cb)
return this
return this.listen(this._emitterWeb, 'onWebview', eventName, cb)
}
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}`)
}
this._emitterLocal.on(eventName, cb)
return this
return this.listen(this._emitterLocal, 'onSelf', eventName, cb)
}
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}`)
}
this._emitterLocal.off(eventName)
return this
return this.unlisten(this._emitterLocal, 'offSelf', eventName)
}
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)
+173 -212
View File
@@ -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)
}
if (e.data.origin === RPCEvents.LISTENER_SERVER) {
this._handleServer(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
}
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}`)
}
this._emitterLocal.on(eventName, cb)
return this
return this.listen(this._emitterLocal, 'onSelf', eventName, cb)
}
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}`)
}
this._emitterLocal.off(eventName)
return this
return this.unlisten(this._emitterLocal, 'offSelf', eventName)
}
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}`,
-51
View File
@@ -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 | '))
}
}
+39
View File
@@ -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
}
+52 -54
View File
@@ -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,62 +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) {
case 'server':
this.operator = new RPCInstanceServer(opts as RPCConfig<'server'>)
break
case 'client':
this.operator = new RPCInstanceClient(opts as RPCConfig<'client'>)
break
case 'webview':
this.operator = new RPCInstanceWebview(opts as RPCConfig<'webview'>)
break
default:
throw new Error(RPCErrors.UNKNOWN_ENVIRONMENT)
}
}
public get(): RPCEnvironmentResolved<T> {
return this.operator as RPCEnvironmentResolved<T>
export function createRPC<T extends RPCEnvironment>(
config: RPCConfig<T>,
): RPCEnvironmentResolved<T> {
switch (config.env) {
case 'server':
return new RPCInstanceServer(
config as RPCConfig<'server'>,
) as RPCEnvironmentResolved<T>
case 'client':
return new RPCInstanceClient(
config as RPCConfig<'client'>,
) as RPCEnvironmentResolved<T>
case 'webview':
return new RPCInstanceWebview(
config as RPCConfig<'webview'>,
) as RPCEnvironmentResolved<T>
default:
throw new RPCError(
RPCErrors.UNKNOWN_ENVIRONMENT,
unknownEnvironmentMessage(config.env),
)
}
}
export { RPCFactory }
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'
+21 -29
View File
@@ -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)
}
if (once) {
this._storage.delete(event)
}
Promise.resolve(cb(...args))
.then(res)
.catch(rej)
})
return (await cb(...(args as never[]))) as R
}
}
+100
View File
@@ -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
View File
@@ -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
+317 -309
View File
@@ -1,321 +1,329 @@
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[] =
[
'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',
] as const
export const NATIVE_CLIENT_NETWORK_EVENTS = [
'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',
] as const
+84
View File
@@ -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
View File
@@ -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]
+47
View File
@@ -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
View File
@@ -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"]
}
+11
View File
@@ -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,
})
-14
View File
@@ -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: [/.*/],
})
+58
View File
@@ -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 {}
+32 -15
View File
@@ -1,34 +1,51 @@
{
"name": "@entityseven/fivem-rpc-shared-types",
"description": "Shared (enhanced) 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"
"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
}
}
}
+42 -149
View File
@@ -1,168 +1,61 @@
# FiveM RPC Shared Types
### [Docs & Info](https://github.com/rilaxik/fivem-rpc/blob/master/readme.md)
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
```bash
pnpm i @entityseven/fivem-rpc-shared-types -D
```
```bash
yarn add @entityseven/fivem-rpc-shared-types --dev
```
```bash
bun add @entityseven/fivem-rpc-shared-types -d
```
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
This package is an enhanced type support for `@entityseven/fivem-rpc`. It provides ability to strictly type your events for better dx.
## Example
This example is neat way to follow up but you can change it as you wish. It will use bun workspaces, pnpm workspaces work in a similar manner. Referring the following folder structure:
```markdown
apps/
- server/
- package.json
- tsconfig.json
- client/
- package.json
- tsconfig.json
- webview/
- package.json
- tsconfig.json
- shared/ (this must be available in server, client and webview)
- package.json
1. Create one declaration file shared by server, client and webview code, e.g. `shared/rpc.d.ts`:
- package.json (root package)
- pnpm-workspace.yaml (only if using pnpm)
```
```ts
import '@entityseven/fivem-rpc-shared-types'
- Environment folder: folder with server, client or webview code in it
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
}
}
```
1. Install this package as dev dependency in your root package or in each environment folder separately
```markdown
apps/
- server/ <- (if not installed root)
- client/ <- (if not installed root)
- webview/ <- (if not installed root)
- shared/
The `import` line makes the file a module, so `declare module` adds to the package's interfaces instead of replacing them
- package.json <- here
```
2. Add the file to `include` in the `tsconfig.json` of every environment:
2. In `shared/` create folder `fivem-rpc` (or similar), inside it create `index.d.ts`
```markdown
apps/
- server/
- client/
- webview/
- shared/
- fivem-rpc/
- index.d.ts <- here
```json
{
"include": ["src", "../shared/rpc.d.ts"]
}
```
- package.json
```
3. Calls are checked from now on:
3. In `index.d.ts` add following:
```ts
declare module '@entityseven/fivem-rpc-shared-types' {
// Client commands names
export type RPCCommands_Client = ''
```ts
// server
rpc.onClient('buyItem', (player, item, amount) => {
// item: string, amount: number, must return boolean
return true
})
// Server commands names
export type RPCCommands_Server = ''
// 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
```
// Client -> Client events
export interface RPCEvents_Client {}
## Interfaces
// Client -> Server events
export interface RPCEvents_ClientServer {}
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
// Client -> Webview events
export interface RPCEvents_ClientWebview {}
- 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`)
// Server -> Server events
export interface RPCEvents_Server {}
## License
// Server -> Client events
export interface RPCEvents_ServerClient {}
// Server -> Server events
export interface RPCEvents_ServerWebview {}
// Webview -> Webview events
export interface RPCEvents_Webview {}
// Webview -> Client events
export interface RPCEvents_WebviewClient {}
// Webview -> Server events
export interface RPCEvents_WebviewServer {}
}
```
4. We just created a declaration which will overwrite types from the package. Now we need our packages to refer to these types when linting `rpc` functions. To do this in each environment folder of your project in `tsconfig.json` do these:
```json5
{
"compilerOptions": {
"types": [
"../shared/fivem-rpc/" // or your specific folder
]
}
}
```
5. We also need to populate interfaces we created in step 3.
- `RPCCommands_Client` and `RPCCommands_Server` will include your commands names an union strings:
```ts
export type RPCCommands_Client = 'afk' | 'vanish' | '...' // example names
export type RPCCommands_Server = 'report' | 'ban' | '...' // example names
```
- Other interfaces will include your events types. The example will show one but all of the work same way
```ts
export interface RPCEvents_ClientServer {
clientToServerEventName(data: string, moreData: boolean): number
"client-to-server-event-name"(data: string, moreData: boolean): number // can also include characters you cannot use as variable or function names
}
```
- `clientToServerEventName` or `client-to-server-event-name` is an event name
- `data` and `moreData` are the arguments you need to pass when calling an event and argument you will receive when listening (may also include extra, as player, check type hints)
- `number` is a return type that will be forwarded back to caller
Doing this will create type hints for you:
```ts
// assuming this is in client
const response /* number */ = await rpc.emitServer(
'clientToServerEventName', /* suggested name */
'data', /* will pass typecheck */
'moreData', /* will NOT pass typecheck, since required type is `boolean` */
)
```
## Example (alternative)
If previous example does not work or you do not like you can also try it this way. Steps that are not mentioned are the same as previous
2. In `shared/` create folders `declarations/fivem-rpc`, inside it create `index.d.ts`
```markdown
apps/
- server/
- client/
- webview/
- shared/
- declarations/
- fivem-rpc/
- index.d.ts <- here
- package.json
```
4. We just created a declaration which will overwrite types from the package. Now we need our packages to refer to these types when linting `rpc` functions. To do this in each environment folder of your project in `tsconfig.json` do these:
```json5
{
"compilerOptions": {
"typeRoots": [
"../shared/declarations/", // or your specific folder
"../../node_modules/@types", // you may also want to add this if some of your other libraries are not showing types now
]
}
}
```
If a any point this stops working for you, do your research on how to redeclare library types and refer to it
Licensed under the [Custom Attribution-NoDerivs Software License](license.md)
-52
View File
@@ -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
}
}