37 Commits
Author SHA1 Message Date
rilaxik d1d4d91a5e chore: bump dependency version 2026-09-28 02:10:14 +01:00
rilaxik 9f949d3950 chore: bump versions 2026-09-28 02:05:40 +01:00
rilaxik 28b1f96b50 feat(rpc): introduce agent skill for extended usage 2026-09-28 02:05:40 +01:00
rilaxik 9a9667c480 docs: move direction matrix to root readme.md 2026-09-28 02:05:40 +01:00
rilaxik cca07468d5 docs: TSdoc 2026-09-28 02:05:40 +01:00
rilaxik 40c322e4b7 docs: bump readme.md + metadata 2026-09-28 02:05:40 +01:00
rilaxik c4c9d02fd4 docs: bump readme.md 2026-09-28 02:05:40 +01:00
rilaxik 674eb25000 docs: bump readme.md 2026-09-28 02:05:40 +01:00
rilaxik 269269771d docs: bump readme.md 2026-09-28 02:05:40 +01:00
rilaxik d189651689 feat: enforce stricter repo-wide typecheck 2026-09-28 02:05:40 +01:00
rilaxik ed66e61de0 chore(rpc): add explanation comments 2026-09-28 02:05:40 +01:00
rilaxik 143bee3e4e chore(rpc): drop dead code 2026-09-28 02:05:40 +01:00
rilaxik 79fe874c90 fix(rpc): remove dead RPCStateRaw branch 2026-09-28 02:05:40 +01:00
rilaxik 50b96eeb80 feat(rpc): change redundant init factory class to function 2026-09-28 02:05:40 +01:00
rilaxik 8029f24e9d feat(rpc): rework shared-types + include as mandatory dep 2026-09-28 02:05:40 +01:00
rilaxik 3e3548a9cc fix(rpc): rework native events + dedupe 2026-09-28 02:05:40 +01:00
rilaxik c085fd2c8f fix(rpc): separate public calls + better pending handlers 2026-09-28 02:05:40 +01:00
rilaxik 6247b7d781 fix(rpc): prevent client id spoofing 2026-09-28 02:05:40 +01:00
rilaxik b48d3f5240 fix(rpc): improve handlers 2026-09-28 02:05:40 +01:00
rilaxik 558f04567a chore(rpc): promote Wrapper to BaseInstance 2026-09-28 02:05:40 +01:00
rilaxik 571d113386 fix(rpc): better error handling 2026-09-28 02:05:40 +01:00
rilaxik 79faa3b23c feat(rpc): extract _pending* maps to Pending util 2026-09-28 02:05:40 +01:00
rilaxik 46933c1656 fix(rpc): rewrite Emitter 2026-09-28 02:05:40 +01:00
rilaxik 2f59e4c253 fix(rpc): move injected @citizenfx/* types to separate file 2026-09-28 02:05:40 +01:00
rilaxik 75cbdb512c chore(repo): package.json maintenance 2026-09-28 02:05:40 +01:00
rilaxik b39a1d6e1b chore(repo): migrate from tsup to tsdown 2026-09-28 02:05:40 +01:00
rilaxik e88af4aa01 chore(rpc): tsconfig.json maintenance 2026-09-28 02:05:40 +01:00
rilaxik b2d086e18e chore(rpc): package.json maintenance 2026-09-28 02:05:40 +01:00
rilaxik e6f9f07f4f chore(repo): bump typescript version 2026-09-28 02:05:40 +01:00
rilaxik 3b695eb561 chore(rpc): pass oxc checks 2026-09-28 02:05:40 +01:00
rilaxik 14c95dd3b4 chore(repo): migrate biome to oxfmt + oxlint 2026-09-28 02:05:39 +01:00
rilaxik f5d390050a chore(repo): config files to standartize commits 2026-09-28 02:05:39 +01:00
rilaxik 67ea24ab33 chore: remove bun.lock from .gitignore 2026-09-27 21:50:21 +01:00
rilaxik 3f7311ba17 readme wip 2025-07-16 20:41:35 +01:00
rilaxik 00b8edc16b docs 2025-07-09 16:07:02 +01:00
rilaxik 91a72c46e8 docs 2025-07-09 00:36:26 +01:00
rilaxik 8a361f67ec cleanup + better descriptions 2025-07-08 23:52:42 +01:00
34 changed files with 2835 additions and 1672 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"
}
}
+87
View File
@@ -0,0 +1,87 @@
# FiveM RPC
Call FiveM server, client and NUI listeners like async functions: typed, with timeouts, no event ping-pong
## Motivation
The idea was to create an extensible package, with various features to simplify the development process and provide as much comfort as possible. Inspired by usage of [altv-xrpc](https://github.com/xxshady/altv-xrpc)
## Packages
| Package | Docs |
| ----------------------------------------------------- | -------------------------------------- |
| [`@entityseven/fivem-rpc`](rpc) | [API reference](rpc/readme.md) |
| [`@entityseven/fivem-rpc-shared-types`](shared-types) | [Typing setup](shared-types/readme.md) |
## Installation
```bash
npm i @entityseven/fivem-rpc
pnpm add @entityseven/fivem-rpc
yarn add @entityseven/fivem-rpc
bun add @entityseven/fivem-rpc
```
Optional, for typed event names, arguments and results ([typing setup](shared-types/readme.md)):
```bash
npm i -D @entityseven/fivem-rpc-shared-types
pnpm add -D @entityseven/fivem-rpc-shared-types
yarn add -D @entityseven/fivem-rpc-shared-types
bun add -d @entityseven/fivem-rpc-shared-types
```
Upgrading from 0.1: [migration guide](migration.md)
## Quick start
Create exactly one instance per environment and import it from your own module, not from the library. The client needs one even if it only relays between server and webview.
```ts
// server/rpc.ts
import { createRPC } from '@entityseven/fivem-rpc'
export const rpc = createRPC({ env: 'server' })
// client/rpc.ts
import { createRPC } from '@entityseven/fivem-rpc'
export const rpc = createRPC({ env: 'client' })
// webview/rpc.ts
import { createRPC } from '@entityseven/fivem-rpc'
export const rpc = createRPC({ env: 'webview' })
```
Listen on one side, emit from the other and await the listener's return value:
```ts
// server
rpc.onClient('ping', (player, message) => `pong: ${message} (from ${player})`)
// client
const reply = await rpc.emitServer('ping', 'hello')
```
All methods: [API reference](rpc/readme.md).
## Features
- Type-Safe Development: Eliminate runtime errors and enhance code reliability with comprehensive type safety
- All-in-one package: Communicate effortlessly between server, client and webview
## Contributing
Issues and pull requests are very welcome
When the API changes, update the TSDoc, the [direction table](rpc/readme.md#directions) and the [agent skill](rpc/skills/fivem-rpc/SKILL.md)
Releases are published with `bun publish`, which replaces the `workspace:^` dependency on shared-types with its version (`npm publish` would not)
## License
Licensed under the [Custom Attribution-NoDerivs Software License](license.md)
## Roadmap
- client observers to catch events between server and webview (subscribe-like behaviour)
- client observers to prevent events (middleware-like behaviour)
- player manager (transform player id to desired data straight from a listener)
+46 -20
View File
@@ -1,43 +1,69 @@
{
"name": "@entityseven/fivem-rpc",
"description": "FiveM RPC is an abstraction for handling events in GTA V 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
}
}
}
+436
View File
@@ -0,0 +1,436 @@
# FiveM RPC
Call FiveM server, client and NUI listeners like async functions: typed, with timeouts, no event ping-pong
Installation, quick start and package overview: [main readme](../readme.md). Typed events: [shared-types](../shared-types/readme.md). Upgrading from 0.1: [migration guide](../migration.md).
## Exports
Besides `createRPC` the package exports:
- `RPCError`, `RPCErrors`, `RPCErrorDetails` - see [Errors](#errors)
- `RPCConfig`, `RPCEnvironment` and the instance types `RPCInstanceServer`, `RPCInstanceClient`, `RPCInstanceWebview`
- native event types `RPCNativeServerEvents`, `RPCNativeClientEvents`, `RPCNativeClientNetworkEvents`, `RPCNativeClientNetworkEventsNames` and the lists `NATIVE_SERVER_EVENTS`, `NATIVE_CLIENT_EVENTS`, `NATIVE_CLIENT_NETWORK_EVENTS` accepted by the `onNative*` methods
## RPCConfig
```ts
type RPCConfig = {
env: 'server' | 'client' | 'webview'
debug?: boolean // default false, logs every registration, call and incoming payload
timeout?: number // default 5000, ms to wait for a response, 0 disables it
}
```
An unknown `env` makes `createRPC` throw `RPCError` with code `RPCErrors.UNKNOWN_ENVIRONMENT`
## Errors
Every error from this library is an `RPCError`. `code` is one of `RPCErrors`, `message` says what to fix, and `details` names the call when the error comes from one
```ts
enum RPCErrors {
EVENT_NOT_REGISTERED = 'Event not registered',
UNKNOWN_NATIVE = 'Unknown native event',
UNKNOWN_ENVIRONMENT = 'Unknown environment',
TIMEOUT = 'Timed out waiting for response',
HANDLER_ERROR = 'Listener threw an error',
}
```
### Example error
The server has no `onClient('buyItem', ...)` listener, so the call from the client rejects:
```ts
import { RPCError, RPCErrors } from '@entityseven/fivem-rpc'
try {
await rpc.emitServer('buyItem', 'water')
} catch (e) {
if (e instanceof RPCError && e.code === RPCErrors.EVENT_NOT_REGISTERED) {
e.message // 'No listener for "buyItem" on server. Register it with rpc.onClient("buyItem", ...) in server code.'
e.details // { event: 'buyItem', uuid: '<uuid>', from: 'client', to: 'server' }
}
}
```
## How it works
### Directions
Every call goes from an `emit*` method in one environment to the matching `on*` listener in another. The last column is the [shared-types](../shared-types/readme.md) interface that types it
| From | Call | To | Listener | Typed by |
| ------- | ------------------------------------- | ----------- | ------------------------------------- | ------------------------- |
| server | `emitClient(player, event, ...args)` | client | `onServer` | `RPCEvents_ServerClient` |
| server | `emitClientEveryone(event, ...args)` | all clients | `onServer`, no response | `RPCEvents_ServerClient` |
| server | `emitWebview(player, event, ...args)` | webview | `onServer`, via client | `RPCEvents_ServerWebview` |
| server | `emitSelf(event, ...args)` | server | `onSelf` | `RPCEvents_Server` |
| client | `emitServer(event, ...args)` | server | `onClient`, player first | `RPCEvents_ClientServer` |
| client | `emitWebview(event, ...args)` | webview | `onClient` | `RPCEvents_ClientWebview` |
| client | `emitSelf(event, ...args)` | client | `onSelf` | `RPCEvents_Client` |
| webview | `emitServer(event, ...args)` | server | `onWebview`, player first, via client | `RPCEvents_WebviewServer` |
| webview | `emitClient(event, ...args)` | client | `onWebview` | `RPCEvents_WebviewClient` |
| webview | `emitSelf(event, ...args)` | webview | `onSelf` | `RPCEvents_Webview` |
Commands registered with `onCommand` are typed by `RPCCommands_Server` and `RPCCommands_Client`
### Routing
Server and client talk over FiveM network events, client and webview over NUI messages and NUI callbacks. Webview and server never talk directly: every call between them is relayed by the client of that player. So every client must run `createRPC({ env: 'client' })`, even with no listeners of its own, or those calls time out
### One listener per event
Each `on*` method keeps one listener per event name. Registering the same name again replaces the previous listener, `off*` removes it. Directions are separate: `onClient('x')` and `onWebview('x')` on the server do not replace each other
### Responses, errors and timeouts
- `emit*` resolves with the value the listener returns (promises are awaited)
- no listener on the target: the call rejects with `RPCErrors.EVENT_NOT_REGISTERED`
- the listener throws: the target logs the error with `console.error`, the call rejects with `RPCErrors.HANDLER_ERROR` and the original message
- no response within `RPCConfig.timeout` (default 5000 ms): the call rejects with `RPCErrors.TIMEOUT` and a late response is ignored. `timeout: 0` waits forever
- `emitSelf` calls the local listener directly, whatever it throws reaches the caller unchanged
- `emitClientEveryone` does not wait for clients: it resolves once sent, failures stay on each client (`console.error` for a throwing listener, the rest with `debug: true`)
### Player identity
Server listeners (`onClient`, `onWebview`) get the calling player's server id as the first argument. It comes from FiveM's `source`, never from the payload, so a client cannot pose as another player. Use it instead of player ids passed as arguments. A response to `emitClient` or `emitWebview` is only accepted from the player it was sent to
## Server ([source](src/core/server.ts))
### onClient
Listens to client event
```ts
rpc.onClient('clientServerEvent', (player, arg1, arg2, ...rest) => {
// logic
return someData // this will be forwarded back to caller
})
```
### offClient
Stops listening to client event
```ts
rpc.offClient('clientServerEvent')
```
### emitClient
Sends event to specified client
```ts
const response = await rpc.emitClient(playerServerId, 'serverClientEvent', someData)
// response will come from client listener with returned data
```
### emitClientEveryone
Sends event to all clients. One-way: clients run their listener but do not answer
```ts
await rpc.emitClientEveryone('serverClientEvent', someData)
```
### onWebview
Listens to webview event
```ts
rpc.onWebview('webviewServerEvent', (player, arg1, arg2, ...rest) => {
// logic
return someData // this will be forwarded back to caller
})
```
### offWebview
Stops listening to webview event
```ts
rpc.offWebview('webviewServerEvent')
```
### emitWebview
Sends event to the webview of specified player
```ts
const response = await rpc.emitWebview(playerServerId, 'serverWebviewEvent', someData)
// response will come from webview listener with returned data
```
### onSelf
Listens to server event
```ts
rpc.onSelf('serverEvent', (arg1, arg2, ...rest) => {
// logic
return someData // this will be forwarded back to caller
})
```
### offSelf
Stops listening to server event
```ts
rpc.offSelf('serverEvent')
```
### emitSelf
Sends event to server
```ts
const response = await rpc.emitSelf('serverEvent', someData)
// response will come from server listener with returned data
```
### onCommand
Registers chat command. `args` are the raw strings typed after the command, validate them yourself. With `restricted` set to `true` only players with the ACE permission `command.<name>` can use it (defaults to `false`)
```ts
rpc.onCommand('serverCommand', (player, args, rawCommand) => {
// logic
}, true /* restricted */)
```
### onNativeEvent
Listens to native server event ([reference](https://docs.fivem.net/docs/scripting-reference/events/server-events/))
```ts
rpc.onNativeEvent('playerJoining', (source, oldId) => {
// logic
})
```
## Client ([source](src/core/client.ts))
### onServer
Listens to server event
```ts
rpc.onServer('serverClientEvent', (arg1, arg2, ...rest) => {
// logic
return someData // this will be forwarded back to caller
})
```
### offServer
Stops listening to server event
```ts
rpc.offServer('serverClientEvent')
```
### emitServer
Sends event to server
```ts
const response = await rpc.emitServer('clientServerEvent', someData)
// response will come from server listener with returned data
```
### onWebview
Listens to webview event
```ts
rpc.onWebview('webviewClientEvent', (arg1, arg2, ...rest) => {
// logic
return someData // this will be forwarded back to caller
})
```
### offWebview
Stops listening to webview event
```ts
rpc.offWebview('webviewClientEvent')
```
### emitWebview
Sends event to own webview
```ts
const response = await rpc.emitWebview('clientWebviewEvent', someData)
// response will come from webview listener with returned data
```
### onSelf
Listens to client event
```ts
rpc.onSelf('clientEvent', (arg1, arg2, ...rest) => {
// logic
return someData // this will be forwarded back to caller
})
```
### offSelf
Stops listening to client event
```ts
rpc.offSelf('clientEvent')
```
### emitSelf
Sends event to client
```ts
const response = await rpc.emitSelf('clientEvent', someData)
// response will come from client listener with returned data
```
### onCommand
Registers chat command. `args` are the raw strings typed after the command, validate them yourself
```ts
rpc.onCommand('clientCommand', (player, args, rawCommand) => {
// logic
})
```
### onNativeEvent
Listens to native client event ([reference](https://docs.fivem.net/docs/scripting-reference/events/client-events/))
```ts
rpc.onNativeEvent('entityDamaged', (victim, culprit, weapon, baseDamage) => {
// logic
})
```
### onNativeNetworkEvent
Listens to native client network event ([reference](https://docs.fivem.net/docs/game-references/game-events/))
```ts
rpc.onNativeNetworkEvent('CEventShockingCarCrash', (entities, eventEntity, data) => {
// logic
})
```
### setWebviewFocus
Sets or removes focus and cursor from own webview
```ts
rpc.setWebviewFocus(true /* focus */, true /* show cursor */)
```
## Webview ([source](src/core/webview.ts))
### onClient
Listens to client event
```ts
rpc.onClient('clientWebviewEvent', (arg1, arg2, ...rest) => {
// logic
return someData // this will be forwarded back to caller
})
```
### offClient
Stops listening to client event
```ts
rpc.offClient('clientWebviewEvent')
```
### emitClient
Sends event to own client
```ts
const response = await rpc.emitClient('webviewClientEvent', someData)
// response will come from client listener with returned data
```
### onServer
Listens to server event
```ts
rpc.onServer('serverWebviewEvent', (arg1, arg2, ...rest) => {
// logic
return someData // this will be forwarded back to caller
})
```
### offServer
Stops listening to server event
```ts
rpc.offServer('serverWebviewEvent')
```
### emitServer
Sends event to server
```ts
const response = await rpc.emitServer('webviewServerEvent', someData)
// response will come from server listener with returned data
```
### onSelf
Listens to webview event
```ts
rpc.onSelf('webviewEvent', (arg1, arg2, ...rest) => {
// logic
return someData // this will be forwarded back to caller
})
```
### offSelf
Stops listening to webview event
```ts
rpc.offSelf('webviewEvent')
```
### emitSelf
Sends event to webview
```ts
const response = await rpc.emitSelf('webviewEvent', someData)
// response will come from webview listener with returned data
```
## Using with AI agents
The package ships an [Agent Skill](https://agentskills.io) with the directions, rules, typing and error fixes above. Copy it into your agent's skills folder, e.g. for Claude Code:
```bash
cp -r node_modules/@entityseven/fivem-rpc/skills/fivem-rpc .claude/skills/
```
Copy it again after upgrading. Every method also carries TSDoc with its direction and matching listener
## License
Licensed under the [Custom Attribution-NoDerivs Software License](license.md)
+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 -57
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,65 +10,60 @@ import {
} from './utils/types'
/**
* RPC Factory
* Creates the RPC instance for one environment. Create exactly one per
* environment (server, client, webview) and export it from a local module.
* Every client needs one, even without listeners: it relays calls between
* its webview and the server.
*
* @returns `RPCInstanceServer`, `RPCInstanceClient` or `RPCInstanceWebview`,
* matching `config.env`
* @throws {@link RPCError} `UNKNOWN_ENVIRONMENT` if `config.env` is not
* `'server'`, `'client'` or `'webview'`
*
* @example
* // returns RPCInstanceServer
* const rpc = new RPCFactory({ env: "server" }).get()
*
* @example
* // returns RPCInstanceClient
* const rpc = new RPCFactory({ env: "client" }).get()
*
* @example
* // returns RPCInstanceWebview
* const rpc = new RPCFactory({ env: "webview" }).get()
*
* @class
* // server/rpc.ts
* import { createRPC } from '@entityseven/fivem-rpc'
* export const rpc = createRPC({ env: 'server' })
*/
class RPCFactory<T extends RPCEnvironment> extends Wrapper {
private readonly operator:
| RPCInstanceServer
| RPCInstanceClient
| RPCInstanceWebview
/**
* Instance options
* @param {object} opts - Options
* @param {string} opts.env - Instance environment
* @param {boolean} opts.debug - Show additional logs
*/
constructor(opts: RPCConfig<T>) {
super(opts)
this.console.log('[RPC] Initializing...')
switch (opts.env) {
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 const rpcClient = new RPCFactory({ env: "client" }).get();
// export const rpcServer = new RPCFactory({ env: "server" }).get();
// export const rpcWebview = new RPCFactory({ env: "webview" }).get();
export * from './utils/types'
export * from './utils/native'
export type * from './core/server'
export type * from './core/client'
export type * from './core/webview'
export type { RPCInstanceClient } from './core/client'
export type { RPCInstanceServer } from './core/server'
export type { RPCInstanceWebview } from './core/webview'
export { RPCError, type RPCErrorDetails } from './utils/errors'
export {
NATIVE_CLIENT_EVENTS,
NATIVE_CLIENT_NETWORK_EVENTS,
NATIVE_SERVER_EVENTS,
} from './utils/native'
export {
type RPCConfig,
type RPCEnvironment,
RPCErrors,
type RPCNativeClientEvents,
type RPCNativeClientNetworkEvents,
type RPCNativeClientNetworkEventsNames,
type RPCNativeServerEvents,
} from './utils/types'
+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 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
}
}
}
+61
View File
@@ -0,0 +1,61 @@
# FiveM RPC Shared Types
Event and command declarations for [`@entityseven/fivem-rpc`](../rpc/readme.md). With nothing declared, every rpc method accepts any event name, any arguments and any result. Declare your events once and every `on*` and `emit*` call gets checked names, arguments and results
## Installation
See the [main readme](../readme.md#installation). The declaration file below imports this package, so it must resolve from that file (in a workspace: install it in the root)
## Usage
1. Create one declaration file shared by server, client and webview code, e.g. `shared/rpc.d.ts`:
```ts
import '@entityseven/fivem-rpc-shared-types'
declare module '@entityseven/fivem-rpc-shared-types' {
interface RPCEvents_ClientServer {
// event name(arguments): value returned by the listener
buyItem(item: string, amount: number): boolean
}
interface RPCCommands_Server {
ban: true
}
}
```
The `import` line makes the file a module, so `declare module` adds to the package's interfaces instead of replacing them
2. Add the file to `include` in the `tsconfig.json` of every environment:
```json
{
"include": ["src", "../shared/rpc.d.ts"]
}
```
3. Calls are checked from now on:
```ts
// server
rpc.onClient('buyItem', (player, item, amount) => {
// item: string, amount: number, must return boolean
return true
})
// client
const bought = await rpc.emitServer('buyItem', 'water', 2) // boolean
await rpc.emitServer('buyItem', 'water') // error: missing `amount`
await rpc.emitServer('buyItme', 'water', 2) // error: unknown event
```
## Interfaces
Each interface types one direction, see the [direction table](../rpc/readme.md#directions) for which `emit*` and `on*` methods use it. `RPCCommands_Server` and `RPCCommands_Client` type `onCommand`. An interface you leave empty stays loose (any name, arguments and result), so you can declare them one at a time
- events: the member name is the event name, its parameters are the arguments, its return type is what `emit*` resolves with. Server `onClient` and `onWebview` listeners get the player id before the declared arguments. Names that are not identifiers work too: `'buy-item'(item: string): boolean`
- commands: the key is the command name, the value is not used (`true`)
## License
Licensed under the [Custom Attribution-NoDerivs Software License](license.md)
-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
}
}