fivem-rpc 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +218 -0
- package/dist/client.d.ts +50 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +189 -0
- package/dist/nui.d.ts +44 -0
- package/dist/nui.d.ts.map +1 -0
- package/dist/nui.js +115 -0
- package/dist/server.d.ts +47 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +126 -0
- package/dist/types.d.ts +94 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +0 -0
- package/dist/utils.d.ts +6 -0
- package/dist/utils.d.ts.map +1 -0
- package/package.json +60 -0
package/README.md
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# fivem-rpc
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
Typed RPC for FiveM. Replaces raw `emitNet`/`onNet` calls with typed, Promise-based procedures between server, client and NUI.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
pnpm add fivem-rpc
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Setup
|
|
14
|
+
|
|
15
|
+
Declare your procedures once by augmenting the registry interfaces from `fivem-rpc/types`. Both sides import the same declarations.
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import type { RpcPayload } from "fivem-rpc/types";
|
|
19
|
+
|
|
20
|
+
declare module "fivem-rpc/types" {
|
|
21
|
+
interface ClientToServerRpc {
|
|
22
|
+
"foo:getData": RpcPayload<{ id: number }, { name: string }>;
|
|
23
|
+
"foo:ping": RpcPayload<undefined, undefined>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface ServerToClientRpc {
|
|
27
|
+
"bar:notify": RpcPayload<{ message: string }, { seen: boolean }>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface NuiToClientRpc {
|
|
31
|
+
"ui:getInfo": RpcPayload<undefined, { name: string }>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface ClientToNuiRpc {
|
|
35
|
+
"ui:show": RpcPayload<{ text: string }, { ok: boolean }>;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`RpcPayload<Request, Response>` describes the argument and return value of a procedure. Use `undefined` for no argument or no return value.
|
|
41
|
+
|
|
42
|
+
## Client to Server
|
|
43
|
+
|
|
44
|
+
On the server, register a handler:
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
import { initializeRpc } from "fivem-rpc/server";
|
|
48
|
+
|
|
49
|
+
const { onClientRpc } = initializeRpc();
|
|
50
|
+
|
|
51
|
+
onClientRpc("foo:getData", async (source, { id }) => {
|
|
52
|
+
return { name: "bar" };
|
|
53
|
+
});
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
On the client, call the procedure:
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import { initializeRpc } from "fivem-rpc/client";
|
|
60
|
+
|
|
61
|
+
const { callServerRpc } = initializeRpc();
|
|
62
|
+
|
|
63
|
+
const result = await callServerRpc("foo:getData", { id: 1 });
|
|
64
|
+
if (result.success) {
|
|
65
|
+
console.log(result.data.name);
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Server to Client
|
|
70
|
+
|
|
71
|
+
On the client, register a handler:
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
const { onServerRpc } = initializeRpc();
|
|
75
|
+
|
|
76
|
+
onServerRpc("bar:notify", async ({ message }) => {
|
|
77
|
+
console.log(message);
|
|
78
|
+
return { seen: true };
|
|
79
|
+
});
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
On the server, call the procedure:
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
const { callClientRpc } = initializeRpc();
|
|
86
|
+
|
|
87
|
+
const result = await callClientRpc(playerId, "bar:notify", { message: "hello" });
|
|
88
|
+
if (result.success) {
|
|
89
|
+
console.log(result.data.seen);
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## NUI ↔ Client
|
|
94
|
+
|
|
95
|
+
### NUI to Client
|
|
96
|
+
|
|
97
|
+
On the client, register a handler:
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
// client script
|
|
101
|
+
const { onNuiRpc } = initializeRpc();
|
|
102
|
+
|
|
103
|
+
onNuiRpc("ui:getInfo", async () => {
|
|
104
|
+
return { name: GetPlayerName("-1") };
|
|
105
|
+
});
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
In NUI, call the procedure:
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
// browser (fivem-rpc/nui)
|
|
112
|
+
import { initializeRpc } from "fivem-rpc/nui";
|
|
113
|
+
|
|
114
|
+
const { callClientRpc } = initializeRpc();
|
|
115
|
+
|
|
116
|
+
const result = await callClientRpc("ui:getInfo");
|
|
117
|
+
if (result.success) {
|
|
118
|
+
document.title = result.data.name;
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Client to NUI
|
|
123
|
+
|
|
124
|
+
In NUI, register a handler:
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
// browser (fivem-rpc/nui)
|
|
128
|
+
const { onClientRpc } = initializeRpc();
|
|
129
|
+
|
|
130
|
+
onClientRpc("ui:show", async ({ text }) => {
|
|
131
|
+
showNotification(text);
|
|
132
|
+
return { ok: true };
|
|
133
|
+
});
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
On the client, call the procedure:
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
// client script
|
|
140
|
+
const { callNuiRpc } = initializeRpc();
|
|
141
|
+
|
|
142
|
+
const result = await callNuiRpc("ui:show", { text: "hello" });
|
|
143
|
+
if (result.success) {
|
|
144
|
+
console.log(result.data.ok);
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Removing handlers
|
|
149
|
+
|
|
150
|
+
All `onXxxRpc` functions have a corresponding `offXxxRpc`. Call it with just the procedure name to unconditionally remove the handler, or pass the original handler reference to only remove it if it is still the currently registered one.
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
const handler = async () => ({ name: "bar" });
|
|
154
|
+
|
|
155
|
+
onClientRpc("foo:getData", handler);
|
|
156
|
+
|
|
157
|
+
// Remove unconditionally:
|
|
158
|
+
offClientRpc("foo:getData");
|
|
159
|
+
|
|
160
|
+
// Remove only if this is still the registered handler:
|
|
161
|
+
offClientRpc("foo:getData", handler);
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
| Entry point | Register | Unregister |
|
|
165
|
+
| --- | --- | --- |
|
|
166
|
+
| `fivem-rpc/server` | `onClientRpc` | `offClientRpc` |
|
|
167
|
+
| `fivem-rpc/client` | `onServerRpc`, `onNuiRpc` | `offServerRpc`, `offNuiRpc` |
|
|
168
|
+
| `fivem-rpc/nui` | `onClientRpc` | `offClientRpc` |
|
|
169
|
+
|
|
170
|
+
## Error handling
|
|
171
|
+
|
|
172
|
+
All call functions always resolve. Check `result.success` before accessing `result.data`.
|
|
173
|
+
|
|
174
|
+
On failure, `result.error` is a discriminated union:
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
const result = await callServerRpc("foo:getData", { id: 1 });
|
|
178
|
+
|
|
179
|
+
if (!result.success) {
|
|
180
|
+
switch (result.error.code) {
|
|
181
|
+
case "ERR_NO_HANDLER":
|
|
182
|
+
console.error("no handler for", result.error.procedure);
|
|
183
|
+
break;
|
|
184
|
+
case "ERR_TIMEOUT":
|
|
185
|
+
console.error("timed out waiting for", result.error.procedure);
|
|
186
|
+
break;
|
|
187
|
+
case "ERR_HANDLER":
|
|
188
|
+
console.error("handler threw:", result.error.message);
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
| Code | Cause |
|
|
195
|
+
| --- | --- |
|
|
196
|
+
| `ERR_NO_HANDLER` | No handler registered for the procedure |
|
|
197
|
+
| `ERR_TIMEOUT` | Response not received within the timeout |
|
|
198
|
+
| `ERR_HANDLER` | Handler threw at runtime |
|
|
199
|
+
|
|
200
|
+
## Channels
|
|
201
|
+
|
|
202
|
+
`initializeRpc` accepts an optional `channel` name (defaults to `"default"`). A channel scopes all internal event names so that two independent systems in the same resource cannot interfere with each other.
|
|
203
|
+
|
|
204
|
+
Use multiple channels when you want to initialise RPC more than once in the same resource, for example to keep unrelated feature sets isolated:
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
const core = initializeRpc({ channel: "core" });
|
|
208
|
+
const player = initializeRpc({ channel: "player" });
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
The channel name must match on both the server and client side. If you only call `initializeRpc` once per resource, you can omit it entirely and rely on the default.
|
|
212
|
+
|
|
213
|
+
## Options
|
|
214
|
+
|
|
215
|
+
| Option | Default | Description |
|
|
216
|
+
| --- | --- | --- |
|
|
217
|
+
| `channel` | `"default"` | Scopes all event names to this channel |
|
|
218
|
+
| `timeout` | `10000` | Milliseconds to wait for a response before resolving with `ERR_TIMEOUT` |
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { ClientToServerRpc, ServerToClientRpc, NuiToClientRpc, ClientToNuiRpc, RpcPayload, RpcResult } from "./types.js";
|
|
2
|
+
type InitializeRpcOptions = {
|
|
3
|
+
/**
|
|
4
|
+
* Channel name used to scope RPC events.
|
|
5
|
+
* Must match the `channel` passed to `initializeRpc` on the server.
|
|
6
|
+
* @default "default"
|
|
7
|
+
*/
|
|
8
|
+
channel?: string;
|
|
9
|
+
/**
|
|
10
|
+
* Timeout for RPC responses in milliseconds.
|
|
11
|
+
* @default 10000
|
|
12
|
+
*/
|
|
13
|
+
timeout?: number;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Initializes a client-side RPC channel.
|
|
17
|
+
*
|
|
18
|
+
* Each channel is isolated. Calling with the same channel name twice throws.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* const { callServerRpc } = initializeRpc({ channel: "core" });
|
|
22
|
+
* const result = await callServerRpc("core:ping");
|
|
23
|
+
*/
|
|
24
|
+
export declare const initializeRpc: ({ channel, timeout, }?: InitializeRpcOptions) => {
|
|
25
|
+
/**
|
|
26
|
+
* Calls a server-side RPC procedure. Always resolves; inspect `result.success` to branch.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* const result = await callServerRpc("player:getData");
|
|
30
|
+
* if (result.success) console.log(result.data.name);
|
|
31
|
+
* else console.error(result.error.message);
|
|
32
|
+
*/
|
|
33
|
+
callServerRpc: <Key extends keyof ClientToServerRpc>(procedure: Key, ...args: ClientToServerRpc[Key] extends RpcPayload<infer Req, infer _Res> ? Req extends undefined ? [] : [Req] : never) => Promise<RpcResult<ClientToServerRpc[Key] extends RpcPayload<infer _Req, infer Res> ? Res : never>>;
|
|
34
|
+
/** Registers a handler for a server-to-client RPC procedure. */
|
|
35
|
+
onServerRpc: <Key extends keyof ServerToClientRpc>(procedure: Key, handler: (...args: ServerToClientRpc[Key] extends RpcPayload<infer Req, infer _Res> ? Req extends undefined ? [] : [Req] : never) => Promise<ServerToClientRpc[Key] extends RpcPayload<infer _Req, infer Res> ? Res : never>) => void;
|
|
36
|
+
/** Removes a handler for a server-to-client RPC procedure. */
|
|
37
|
+
offServerRpc: <Key extends keyof ServerToClientRpc>(procedure: Key, handler?: (...args: ServerToClientRpc[Key] extends RpcPayload<infer Req, infer _Res> ? Req extends undefined ? [] : [Req] : never) => Promise<ServerToClientRpc[Key] extends RpcPayload<infer _Req, infer Res> ? Res : never>) => void;
|
|
38
|
+
/**
|
|
39
|
+
* Calls a NUI procedure and returns a typed Promise.
|
|
40
|
+
* The NUI must have a corresponding `onClientRpc` handler registered.
|
|
41
|
+
* Always resolves; inspect `result.success` to branch.
|
|
42
|
+
*/
|
|
43
|
+
callNuiRpc: <Key extends keyof ClientToNuiRpc>(procedure: Key, ...args: ClientToNuiRpc[Key] extends RpcPayload<infer Req, infer _Res> ? Req extends undefined ? [] : [Req] : never) => Promise<RpcResult<ClientToNuiRpc[Key] extends RpcPayload<infer _Req, infer Res> ? Res : never>>;
|
|
44
|
+
/** Registers a handler for a NUI-to-client RPC procedure. */
|
|
45
|
+
onNuiRpc: <Key extends keyof NuiToClientRpc>(procedure: Key, handler: (...args: NuiToClientRpc[Key] extends RpcPayload<infer Req, infer _Res> ? Req extends undefined ? [] : [Req] : never) => Promise<NuiToClientRpc[Key] extends RpcPayload<infer _Req, infer Res> ? Res : never>) => void;
|
|
46
|
+
/** Removes a handler for a NUI-to-client RPC procedure. */
|
|
47
|
+
offNuiRpc: <Key extends keyof NuiToClientRpc>(procedure: Key, handler?: (...args: NuiToClientRpc[Key] extends RpcPayload<infer Req, infer _Res> ? Req extends undefined ? [] : [Req] : never) => Promise<NuiToClientRpc[Key] extends RpcPayload<infer _Req, infer Res> ? Res : never>) => void;
|
|
48
|
+
};
|
|
49
|
+
export {};
|
|
50
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region src/utils.ts
|
|
3
|
+
/** Generates a unique call ID scoped to a procedure and channel. */
|
|
4
|
+
const generateCallId = ({ procedure, channel }) => `${channel}:${procedure}:${"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
5
|
+
const r = Math.random() * 16 | 0;
|
|
6
|
+
return (c === "x" ? r : r & 3 | 8).toString(16);
|
|
7
|
+
})}`;
|
|
8
|
+
//#endregion
|
|
9
|
+
//#region src/client.ts
|
|
10
|
+
const activeChannels = /* @__PURE__ */ new Set();
|
|
11
|
+
/**
|
|
12
|
+
* Initializes a client-side RPC channel.
|
|
13
|
+
*
|
|
14
|
+
* Each channel is isolated. Calling with the same channel name twice throws.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* const { callServerRpc } = initializeRpc({ channel: "core" });
|
|
18
|
+
* const result = await callServerRpc("core:ping");
|
|
19
|
+
*/
|
|
20
|
+
const initializeRpc = ({ channel = "default", timeout = 1e4 } = {}) => {
|
|
21
|
+
if (activeChannels.has(channel)) throw new Error(`RPC channel already initialized: "${channel}"`);
|
|
22
|
+
activeChannels.add(channel);
|
|
23
|
+
const request = `__rpc_c2s_${channel}__`;
|
|
24
|
+
const response = `__rpc_c2s_response_${channel}__`;
|
|
25
|
+
const pendingResponses = /* @__PURE__ */ new Map();
|
|
26
|
+
const onRpcResponse = (handler) => onNet(response, handler);
|
|
27
|
+
const emitRpcRequest = (callId, procedure, args) => emitNet(request, callId, procedure, args);
|
|
28
|
+
onRpcResponse((callId, result) => {
|
|
29
|
+
const pending = pendingResponses.get(callId);
|
|
30
|
+
if (!pending) return;
|
|
31
|
+
pendingResponses.delete(callId);
|
|
32
|
+
clearTimeout(pending.timer);
|
|
33
|
+
pending.resolve(result);
|
|
34
|
+
});
|
|
35
|
+
const serverToClientHandlers = /* @__PURE__ */ new Map();
|
|
36
|
+
const nuiToClientHandlers = /* @__PURE__ */ new Map();
|
|
37
|
+
const clientToNuiPending = /* @__PURE__ */ new Map();
|
|
38
|
+
RegisterNuiCallbackType(`__rpc_nui2c_${channel}__`);
|
|
39
|
+
on(`__cfx_nui:__rpc_nui2c_${channel}__`, async (data, cb) => {
|
|
40
|
+
const handler = nuiToClientHandlers.get(data.procedure);
|
|
41
|
+
if (!handler) {
|
|
42
|
+
cb({
|
|
43
|
+
success: false,
|
|
44
|
+
error: {
|
|
45
|
+
code: "ERR_NO_HANDLER",
|
|
46
|
+
procedure: data.procedure
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
cb({
|
|
53
|
+
success: true,
|
|
54
|
+
data: await handler(data.args)
|
|
55
|
+
});
|
|
56
|
+
} catch (e) {
|
|
57
|
+
cb({
|
|
58
|
+
success: false,
|
|
59
|
+
error: {
|
|
60
|
+
code: "ERR_HANDLER",
|
|
61
|
+
message: e instanceof Error ? e.message : String(e)
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
RegisterNuiCallbackType(`__rpc_c2nui_response_${channel}__`);
|
|
67
|
+
on(`__cfx_nui:__rpc_c2nui_response_${channel}__`, (data, cb) => {
|
|
68
|
+
const pending = clientToNuiPending.get(data.callId);
|
|
69
|
+
if (pending) {
|
|
70
|
+
clientToNuiPending.delete(data.callId);
|
|
71
|
+
clearTimeout(pending.timer);
|
|
72
|
+
pending.resolve(data.result);
|
|
73
|
+
}
|
|
74
|
+
cb({});
|
|
75
|
+
});
|
|
76
|
+
onNet(`__rpc_s2c_${channel}__`, async (callId, procedure, args) => {
|
|
77
|
+
const handler = serverToClientHandlers.get(procedure);
|
|
78
|
+
if (!handler) {
|
|
79
|
+
emitNet(`__rpc_s2c_response_${channel}__`, callId, {
|
|
80
|
+
success: false,
|
|
81
|
+
error: {
|
|
82
|
+
code: "ERR_NO_HANDLER",
|
|
83
|
+
procedure
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
const data = await handler(args);
|
|
90
|
+
emitNet(`__rpc_s2c_response_${channel}__`, callId, {
|
|
91
|
+
success: true,
|
|
92
|
+
data
|
|
93
|
+
});
|
|
94
|
+
} catch (e) {
|
|
95
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
96
|
+
emitNet(`__rpc_s2c_response_${channel}__`, callId, {
|
|
97
|
+
success: false,
|
|
98
|
+
error: {
|
|
99
|
+
code: "ERR_HANDLER",
|
|
100
|
+
message
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
return {
|
|
106
|
+
/**
|
|
107
|
+
* Calls a server-side RPC procedure. Always resolves; inspect `result.success` to branch.
|
|
108
|
+
*
|
|
109
|
+
* @example
|
|
110
|
+
* const result = await callServerRpc("player:getData");
|
|
111
|
+
* if (result.success) console.log(result.data.name);
|
|
112
|
+
* else console.error(result.error.message);
|
|
113
|
+
*/
|
|
114
|
+
callServerRpc: (procedure, ...args) => {
|
|
115
|
+
const callId = generateCallId({
|
|
116
|
+
procedure,
|
|
117
|
+
channel
|
|
118
|
+
});
|
|
119
|
+
return new Promise((resolve) => {
|
|
120
|
+
const timer = setTimeout(() => {
|
|
121
|
+
pendingResponses.delete(callId);
|
|
122
|
+
resolve({
|
|
123
|
+
success: false,
|
|
124
|
+
error: {
|
|
125
|
+
code: "ERR_TIMEOUT",
|
|
126
|
+
procedure
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}, timeout);
|
|
130
|
+
pendingResponses.set(callId, {
|
|
131
|
+
resolve,
|
|
132
|
+
timer
|
|
133
|
+
});
|
|
134
|
+
emitRpcRequest(callId, procedure, args[0] ?? null);
|
|
135
|
+
});
|
|
136
|
+
},
|
|
137
|
+
/** Registers a handler for a server-to-client RPC procedure. */
|
|
138
|
+
onServerRpc: (procedure, handler) => {
|
|
139
|
+
serverToClientHandlers.set(procedure, handler);
|
|
140
|
+
},
|
|
141
|
+
/** Removes a handler for a server-to-client RPC procedure. */
|
|
142
|
+
offServerRpc: (procedure, handler) => {
|
|
143
|
+
if (!handler || serverToClientHandlers.get(procedure) === handler) serverToClientHandlers.delete(procedure);
|
|
144
|
+
},
|
|
145
|
+
/**
|
|
146
|
+
* Calls a NUI procedure and returns a typed Promise.
|
|
147
|
+
* The NUI must have a corresponding `onClientRpc` handler registered.
|
|
148
|
+
* Always resolves; inspect `result.success` to branch.
|
|
149
|
+
*/
|
|
150
|
+
callNuiRpc: (procedure, ...args) => {
|
|
151
|
+
const callId = generateCallId({
|
|
152
|
+
procedure,
|
|
153
|
+
channel
|
|
154
|
+
});
|
|
155
|
+
return new Promise((resolve) => {
|
|
156
|
+
const timer = setTimeout(() => {
|
|
157
|
+
clientToNuiPending.delete(callId);
|
|
158
|
+
resolve({
|
|
159
|
+
success: false,
|
|
160
|
+
error: {
|
|
161
|
+
code: "ERR_TIMEOUT",
|
|
162
|
+
procedure
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
}, timeout);
|
|
166
|
+
clientToNuiPending.set(callId, {
|
|
167
|
+
resolve,
|
|
168
|
+
timer
|
|
169
|
+
});
|
|
170
|
+
SendNuiMessage(JSON.stringify({
|
|
171
|
+
type: `__rpc_c2nui_${channel}__`,
|
|
172
|
+
callId,
|
|
173
|
+
procedure,
|
|
174
|
+
args: args[0] ?? null
|
|
175
|
+
}));
|
|
176
|
+
});
|
|
177
|
+
},
|
|
178
|
+
/** Registers a handler for a NUI-to-client RPC procedure. */
|
|
179
|
+
onNuiRpc: (procedure, handler) => {
|
|
180
|
+
nuiToClientHandlers.set(procedure, handler);
|
|
181
|
+
},
|
|
182
|
+
/** Removes a handler for a NUI-to-client RPC procedure. */
|
|
183
|
+
offNuiRpc: (procedure, handler) => {
|
|
184
|
+
if (!handler || nuiToClientHandlers.get(procedure) === handler) nuiToClientHandlers.delete(procedure);
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
};
|
|
188
|
+
//#endregion
|
|
189
|
+
exports.initializeRpc = initializeRpc;
|
package/dist/nui.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { NuiToClientRpc, ClientToNuiRpc, RpcPayload, RpcResult } from "./types.js";
|
|
2
|
+
type InitializeRpcOptions = {
|
|
3
|
+
/**
|
|
4
|
+
* Channel name used to scope RPC events.
|
|
5
|
+
* Must match the `channel` passed to `initializeRpc` on the client.
|
|
6
|
+
* @default "default"
|
|
7
|
+
*/
|
|
8
|
+
channel?: string;
|
|
9
|
+
/**
|
|
10
|
+
* Milliseconds to wait for a response before resolving with ERR_TIMEOUT.
|
|
11
|
+
* @default 10000
|
|
12
|
+
*/
|
|
13
|
+
timeout?: number;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Initializes a NUI-side RPC channel.
|
|
17
|
+
*
|
|
18
|
+
* - {@link callClientRpc} — calls a procedure on the client script (NuiToClientRpc).
|
|
19
|
+
* - {@link onClientRpc} — registers a handler for client-initiated calls (ClientToNuiRpc).
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* const { callClientRpc, onClientRpc } = initializeRpc({ channel: "ui" });
|
|
23
|
+
*
|
|
24
|
+
* onClientRpc("ui:showNotification", async ({ message }) => {
|
|
25
|
+
* showToast(message);
|
|
26
|
+
* return { seen: true };
|
|
27
|
+
* });
|
|
28
|
+
*
|
|
29
|
+
* const result = await callClientRpc("ui:getPlayerInfo");
|
|
30
|
+
* if (result.success) setName(result.data.name);
|
|
31
|
+
*/
|
|
32
|
+
export declare const initializeRpc: ({ channel, timeout, }?: InitializeRpcOptions) => {
|
|
33
|
+
/**
|
|
34
|
+
* Calls a procedure on the client script and returns a typed Promise.
|
|
35
|
+
* Always resolves; inspect `result.success` to branch.
|
|
36
|
+
*/
|
|
37
|
+
callClientRpc: <Key extends keyof NuiToClientRpc>(procedure: Key, ...args: NuiToClientRpc[Key] extends RpcPayload<infer Req, infer _Res> ? Req extends undefined ? [] : [Req] : never) => Promise<RpcResult<NuiToClientRpc[Key] extends RpcPayload<infer _Req, infer Res> ? Res : never>>;
|
|
38
|
+
/** Registers a handler for a client-to-NUI RPC procedure. */
|
|
39
|
+
onClientRpc: <Key extends keyof ClientToNuiRpc>(procedure: Key, handler: (...args: ClientToNuiRpc[Key] extends RpcPayload<infer Req, infer _Res> ? Req extends undefined ? [] : [Req] : never) => Promise<ClientToNuiRpc[Key] extends RpcPayload<infer _Req, infer Res> ? Res : never>) => void;
|
|
40
|
+
/** Removes a handler for a client-to-NUI RPC procedure. */
|
|
41
|
+
offClientRpc: <Key extends keyof ClientToNuiRpc>(procedure: Key, handler?: (...args: ClientToNuiRpc[Key] extends RpcPayload<infer Req, infer _Res> ? Req extends undefined ? [] : [Req] : never) => Promise<ClientToNuiRpc[Key] extends RpcPayload<infer _Req, infer Res> ? Res : never>) => void;
|
|
42
|
+
};
|
|
43
|
+
export {};
|
|
44
|
+
//# sourceMappingURL=nui.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nui.d.ts","sourceRoot":"","sources":["../src/nui.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACX,cAAc,EACd,cAAc,EACd,UAAU,EACV,SAAS,EAET,MAAM,YAAY,CAAC;AAUpB,KAAK,oBAAoB,GAAG;IAC3B;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,aAAa,GAAI,wBAG3B,oBAAyB;IAmD1B;;;OAGG;oBACmB,GAAG,SAAS,MAAM,cAAc,aAC1C,GAAG,WACL,cAAc,CAAC,GAAG,CAAC,SAAS,UAAU,CAAC,MAAM,GAAG,EAAE,MAAM,IAAI,CAAC,GACnE,GAAG,SAAS,SAAS,GACpB,EAAE,GACF,CAAC,GAAG,CAAC,GACN,KAAK,KACN,OAAO,CACT,SAAS,CACR,cAAc,CAAC,GAAG,CAAC,SAAS,UAAU,CAAC,MAAM,IAAI,EAAE,MAAM,GAAG,CAAC,GAC1D,GAAG,GACH,KAAK,CACR,CACD;IAwCD,6DAA6D;kBAC/C,GAAG,SAAS,MAAM,cAAc,aAClC,GAAG,WACL,CACR,GAAG,IAAI,EAAE,cAAc,CAAC,GAAG,CAAC,SAAS,UAAU,CAAC,MAAM,GAAG,EAAE,MAAM,IAAI,CAAC,GACnE,GAAG,SAAS,SAAS,GACpB,EAAE,GACF,CAAC,GAAG,CAAC,GACN,KAAK,KACJ,OAAO,CACX,cAAc,CAAC,GAAG,CAAC,SAAS,UAAU,CAAC,MAAM,IAAI,EAAE,MAAM,GAAG,CAAC,GAC1D,GAAG,GACH,KAAK,CACR;IAQF,2DAA2D;mBAC5C,GAAG,SAAS,MAAM,cAAc,aACnC,GAAG,YACJ,CACT,GAAG,IAAI,EAAE,cAAc,CAAC,GAAG,CAAC,SAAS,UAAU,CAAC,MAAM,GAAG,EAAE,MAAM,IAAI,CAAC,GACnE,GAAG,SAAS,SAAS,GACpB,EAAE,GACF,CAAC,GAAG,CAAC,GACN,KAAK,KACJ,OAAO,CACX,cAAc,CAAC,GAAG,CAAC,SAAS,UAAU,CAAC,MAAM,IAAI,EAAE,MAAM,GAAG,CAAC,GAC1D,GAAG,GACH,KAAK,CACR;CAWH,CAAC"}
|
package/dist/nui.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
//#region src/nui.ts
|
|
2
|
+
/** Tracks initialized channel names to prevent double-initialization. */
|
|
3
|
+
const activeChannels = /* @__PURE__ */ new Set();
|
|
4
|
+
/**
|
|
5
|
+
* Initializes a NUI-side RPC channel.
|
|
6
|
+
*
|
|
7
|
+
* - {@link callClientRpc} — calls a procedure on the client script (NuiToClientRpc).
|
|
8
|
+
* - {@link onClientRpc} — registers a handler for client-initiated calls (ClientToNuiRpc).
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* const { callClientRpc, onClientRpc } = initializeRpc({ channel: "ui" });
|
|
12
|
+
*
|
|
13
|
+
* onClientRpc("ui:showNotification", async ({ message }) => {
|
|
14
|
+
* showToast(message);
|
|
15
|
+
* return { seen: true };
|
|
16
|
+
* });
|
|
17
|
+
*
|
|
18
|
+
* const result = await callClientRpc("ui:getPlayerInfo");
|
|
19
|
+
* if (result.success) setName(result.data.name);
|
|
20
|
+
*/
|
|
21
|
+
const initializeRpc = ({ channel = "default", timeout = 1e4 } = {}) => {
|
|
22
|
+
if (activeChannels.has(channel)) throw new Error(`RPC channel already initialized: "${channel}"`);
|
|
23
|
+
activeChannels.add(channel);
|
|
24
|
+
const resourceName = GetParentResourceName();
|
|
25
|
+
const nuiToClientEndpoint = `https://${resourceName}/__rpc_nui2c_${channel}__`;
|
|
26
|
+
const clientToNuiResponseEndpoint = `https://${resourceName}/__rpc_c2nui_response_${channel}__`;
|
|
27
|
+
const clientToNuiHandlers = /* @__PURE__ */ new Map();
|
|
28
|
+
window.addEventListener("message", async (event) => {
|
|
29
|
+
const data = event.data;
|
|
30
|
+
if (data.type !== `__rpc_c2nui_${channel}__`) return;
|
|
31
|
+
const { callId, procedure, args } = data;
|
|
32
|
+
if (!callId || !procedure) return;
|
|
33
|
+
const handler = clientToNuiHandlers.get(procedure);
|
|
34
|
+
const result = handler ? await handler(args).then((d) => ({
|
|
35
|
+
success: true,
|
|
36
|
+
data: d
|
|
37
|
+
}), (e) => ({
|
|
38
|
+
success: false,
|
|
39
|
+
error: {
|
|
40
|
+
code: "ERR_HANDLER",
|
|
41
|
+
message: e instanceof Error ? e.message : String(e)
|
|
42
|
+
}
|
|
43
|
+
})) : {
|
|
44
|
+
success: false,
|
|
45
|
+
error: {
|
|
46
|
+
code: "ERR_NO_HANDLER",
|
|
47
|
+
procedure
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
await fetch(clientToNuiResponseEndpoint, {
|
|
51
|
+
method: "POST",
|
|
52
|
+
headers: { "Content-Type": "application/json" },
|
|
53
|
+
body: JSON.stringify({
|
|
54
|
+
callId,
|
|
55
|
+
result
|
|
56
|
+
})
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
return {
|
|
60
|
+
/**
|
|
61
|
+
* Calls a procedure on the client script and returns a typed Promise.
|
|
62
|
+
* Always resolves; inspect `result.success` to branch.
|
|
63
|
+
*/
|
|
64
|
+
callClientRpc: async (procedure, ...args) => {
|
|
65
|
+
const controller = new AbortController();
|
|
66
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
67
|
+
try {
|
|
68
|
+
const response = await fetch(nuiToClientEndpoint, {
|
|
69
|
+
method: "POST",
|
|
70
|
+
headers: { "Content-Type": "application/json" },
|
|
71
|
+
body: JSON.stringify({
|
|
72
|
+
procedure,
|
|
73
|
+
args: args[0] ?? null
|
|
74
|
+
}),
|
|
75
|
+
signal: controller.signal
|
|
76
|
+
});
|
|
77
|
+
clearTimeout(timer);
|
|
78
|
+
if (!response.ok) return {
|
|
79
|
+
success: false,
|
|
80
|
+
error: {
|
|
81
|
+
code: "ERR_HANDLER",
|
|
82
|
+
message: `NUI fetch failed: ${response.status}`
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
return await response.json();
|
|
86
|
+
} catch (e) {
|
|
87
|
+
clearTimeout(timer);
|
|
88
|
+
if (e instanceof DOMException && e.name === "AbortError") return {
|
|
89
|
+
success: false,
|
|
90
|
+
error: {
|
|
91
|
+
code: "ERR_TIMEOUT",
|
|
92
|
+
procedure
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
return {
|
|
96
|
+
success: false,
|
|
97
|
+
error: {
|
|
98
|
+
code: "ERR_HANDLER",
|
|
99
|
+
message: e instanceof Error ? e.message : String(e)
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
/** Registers a handler for a client-to-NUI RPC procedure. */
|
|
105
|
+
onClientRpc: (procedure, handler) => {
|
|
106
|
+
clientToNuiHandlers.set(procedure, handler);
|
|
107
|
+
},
|
|
108
|
+
/** Removes a handler for a client-to-NUI RPC procedure. */
|
|
109
|
+
offClientRpc: (procedure, handler) => {
|
|
110
|
+
if (!handler || clientToNuiHandlers.get(procedure) === handler) clientToNuiHandlers.delete(procedure);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
};
|
|
114
|
+
//#endregion
|
|
115
|
+
export { initializeRpc };
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { ClientToServerRpc, RpcPayload, RpcResult, ServerToClientRpc } from "./types.js";
|
|
2
|
+
type InitializeRpcOptions = {
|
|
3
|
+
/**
|
|
4
|
+
* Channel name used to scope RPC events.
|
|
5
|
+
* Must match the `channel` passed to `initializeRpc` on the client.
|
|
6
|
+
* @default "default"
|
|
7
|
+
*/
|
|
8
|
+
channel?: string;
|
|
9
|
+
/**
|
|
10
|
+
* Timeout for S2C RPC responses in milliseconds.
|
|
11
|
+
* @default 10000
|
|
12
|
+
*/
|
|
13
|
+
timeout?: number;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Initializes a server-side RPC channel.
|
|
17
|
+
*
|
|
18
|
+
* Each channel is isolated. Calling with the same channel name twice throws.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* const core = initializeRpc({ channel: "core" });
|
|
22
|
+
* core.onClientRpc("core:ping", async (source) => ({ pong: true }));
|
|
23
|
+
*/
|
|
24
|
+
export declare const initializeRpc: ({ channel, timeout, }?: InitializeRpcOptions) => {
|
|
25
|
+
/**
|
|
26
|
+
* Registers a handler for a client-to-server RPC procedure.
|
|
27
|
+
*
|
|
28
|
+
* Registering the same procedure again overwrites the previous handler.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* onClientRpc("player:getData", async (source) => ({
|
|
32
|
+
* name: GetPlayerName(String(source)),
|
|
33
|
+
* }));
|
|
34
|
+
*/
|
|
35
|
+
onClientRpc: <Key extends keyof ClientToServerRpc>(procedure: Key, handler: (source: number, ...args: ClientToServerRpc[Key] extends RpcPayload<infer Req, infer _Res> ? Req extends undefined ? [] : [Req] : never) => Promise<ClientToServerRpc[Key] extends RpcPayload<infer _Req, infer Res> ? Res : never>) => void;
|
|
36
|
+
/**
|
|
37
|
+
* Removes a handler for a client-to-server RPC procedure.
|
|
38
|
+
* If `handler` is provided, only removes it if it is the currently registered handler.
|
|
39
|
+
*/
|
|
40
|
+
offClientRpc: <Key extends keyof ClientToServerRpc>(procedure: Key, handler?: (source: number, ...args: ClientToServerRpc[Key] extends RpcPayload<infer Req, infer _Res> ? Req extends undefined ? [] : [Req] : never) => Promise<ClientToServerRpc[Key] extends RpcPayload<infer _Req, infer Res> ? Res : never>) => void;
|
|
41
|
+
/**
|
|
42
|
+
* Calls a procedure on a specific client. Always resolves; inspect `result.success` to branch.
|
|
43
|
+
*/
|
|
44
|
+
callClientRpc: <Key extends keyof ServerToClientRpc>(playerId: number, procedure: Key, ...args: ServerToClientRpc[Key] extends RpcPayload<infer Req, infer _Res> ? Req extends undefined ? [] : [Req] : never) => Promise<RpcResult<ServerToClientRpc[Key] extends RpcPayload<infer _Req, infer Res> ? Res : never>>;
|
|
45
|
+
};
|
|
46
|
+
export {};
|
|
47
|
+
//# sourceMappingURL=server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACX,iBAAiB,EAEjB,UAAU,EACV,SAAS,EACT,iBAAiB,EACjB,MAAM,YAAY,CAAC;AAgBpB,KAAK,oBAAoB,GAAG;IAC3B;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,aAAa,GAAI,wBAG3B,oBAAyB;IA8E1B;;;;;;;;;OASG;kBACW,GAAG,SAAS,MAAM,iBAAiB,aACrC,GAAG,WACL,CACR,MAAM,EAAE,MAAM,EACd,GAAG,IAAI,EAAE,iBAAiB,CAAC,GAAG,CAAC,SAAS,UAAU,CACjD,MAAM,GAAG,EACT,MAAM,IAAI,CACV,GACE,GAAG,SAAS,SAAS,GACpB,EAAE,GACF,CAAC,GAAG,CAAC,GACN,KAAK,KACJ,OAAO,CACX,iBAAiB,CAAC,GAAG,CAAC,SAAS,UAAU,CAAC,MAAM,IAAI,EAAE,MAAM,GAAG,CAAC,GAC7D,GAAG,GACH,KAAK,CACR;IAQF;;;OAGG;mBACY,GAAG,SAAS,MAAM,iBAAiB,aACtC,GAAG,YACJ,CACT,MAAM,EAAE,MAAM,EACd,GAAG,IAAI,EAAE,iBAAiB,CAAC,GAAG,CAAC,SAAS,UAAU,CACjD,MAAM,GAAG,EACT,MAAM,IAAI,CACV,GACE,GAAG,SAAS,SAAS,GACpB,EAAE,GACF,CAAC,GAAG,CAAC,GACN,KAAK,KACJ,OAAO,CACX,iBAAiB,CAAC,GAAG,CAAC,SAAS,UAAU,CAAC,MAAM,IAAI,EAAE,MAAM,GAAG,CAAC,GAC7D,GAAG,GACH,KAAK,CACR;IAWF;;OAEG;oBACa,GAAG,SAAS,MAAM,iBAAiB,YACxC,MAAM,aACL,GAAG,WACL,iBAAiB,CAAC,GAAG,CAAC,SAAS,UAAU,CAAC,MAAM,GAAG,EAAE,MAAM,IAAI,CAAC,GACtE,GAAG,SAAS,SAAS,GACpB,EAAE,GACF,CAAC,GAAG,CAAC,GACN,KAAK,KACN,OAAO,CACT,SAAS,CACR,iBAAiB,CAAC,GAAG,CAAC,SAAS,UAAU,CAAC,MAAM,IAAI,EAAE,MAAM,GAAG,CAAC,GAC7D,GAAG,GACH,KAAK,CACR,CACD;CAwBF,CAAC"}
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region src/utils.ts
|
|
3
|
+
/** Generates a unique call ID scoped to a procedure and channel. */
|
|
4
|
+
const generateCallId = ({ procedure, channel }) => `${channel}:${procedure}:${"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
5
|
+
const r = Math.random() * 16 | 0;
|
|
6
|
+
return (c === "x" ? r : r & 3 | 8).toString(16);
|
|
7
|
+
})}`;
|
|
8
|
+
//#endregion
|
|
9
|
+
//#region src/server.ts
|
|
10
|
+
const activeChannels = /* @__PURE__ */ new Set();
|
|
11
|
+
/**
|
|
12
|
+
* Initializes a server-side RPC channel.
|
|
13
|
+
*
|
|
14
|
+
* Each channel is isolated. Calling with the same channel name twice throws.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* const core = initializeRpc({ channel: "core" });
|
|
18
|
+
* core.onClientRpc("core:ping", async (source) => ({ pong: true }));
|
|
19
|
+
*/
|
|
20
|
+
const initializeRpc = ({ channel = "default", timeout = 1e4 } = {}) => {
|
|
21
|
+
if (activeChannels.has(channel)) throw new Error(`RPC channel already initialized: "${channel}"`);
|
|
22
|
+
activeChannels.add(channel);
|
|
23
|
+
const request = `__rpc_c2s_${channel}__`;
|
|
24
|
+
const response = `__rpc_c2s_response_${channel}__`;
|
|
25
|
+
const clientToServerHandlers = /* @__PURE__ */ new Map();
|
|
26
|
+
const onRpcRequest = (handler) => onNet(request, handler);
|
|
27
|
+
const emitRpcResponse = ({ target, callId, result }) => emitNet(response, target, callId, result);
|
|
28
|
+
onRpcRequest(async (callId, procedure, args) => {
|
|
29
|
+
const playerId = source;
|
|
30
|
+
const handler = clientToServerHandlers.get(procedure);
|
|
31
|
+
if (!handler) {
|
|
32
|
+
emitRpcResponse({
|
|
33
|
+
target: playerId,
|
|
34
|
+
callId,
|
|
35
|
+
result: {
|
|
36
|
+
success: false,
|
|
37
|
+
error: {
|
|
38
|
+
code: "ERR_NO_HANDLER",
|
|
39
|
+
procedure
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
emitRpcResponse({
|
|
47
|
+
target: playerId,
|
|
48
|
+
callId,
|
|
49
|
+
result: {
|
|
50
|
+
success: true,
|
|
51
|
+
data: await handler(playerId, args)
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
} catch (e) {
|
|
55
|
+
emitRpcResponse({
|
|
56
|
+
target: playerId,
|
|
57
|
+
callId,
|
|
58
|
+
result: {
|
|
59
|
+
success: false,
|
|
60
|
+
error: {
|
|
61
|
+
code: "ERR_HANDLER",
|
|
62
|
+
message: e instanceof Error ? e.message : String(e)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
const serverToClientPending = /* @__PURE__ */ new Map();
|
|
69
|
+
onNet(`__rpc_s2c_response_${channel}__`, (callId, result) => {
|
|
70
|
+
const pending = serverToClientPending.get(callId);
|
|
71
|
+
if (!pending) return;
|
|
72
|
+
serverToClientPending.delete(callId);
|
|
73
|
+
clearTimeout(pending.timer);
|
|
74
|
+
pending.resolve(result);
|
|
75
|
+
});
|
|
76
|
+
return {
|
|
77
|
+
/**
|
|
78
|
+
* Registers a handler for a client-to-server RPC procedure.
|
|
79
|
+
*
|
|
80
|
+
* Registering the same procedure again overwrites the previous handler.
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* onClientRpc("player:getData", async (source) => ({
|
|
84
|
+
* name: GetPlayerName(String(source)),
|
|
85
|
+
* }));
|
|
86
|
+
*/
|
|
87
|
+
onClientRpc: (procedure, handler) => {
|
|
88
|
+
clientToServerHandlers.set(procedure, handler);
|
|
89
|
+
},
|
|
90
|
+
/**
|
|
91
|
+
* Removes a handler for a client-to-server RPC procedure.
|
|
92
|
+
* If `handler` is provided, only removes it if it is the currently registered handler.
|
|
93
|
+
*/
|
|
94
|
+
offClientRpc: (procedure, handler) => {
|
|
95
|
+
if (!handler || clientToServerHandlers.get(procedure) === handler) clientToServerHandlers.delete(procedure);
|
|
96
|
+
},
|
|
97
|
+
/**
|
|
98
|
+
* Calls a procedure on a specific client. Always resolves; inspect `result.success` to branch.
|
|
99
|
+
*/
|
|
100
|
+
callClientRpc: (playerId, procedure, ...args) => {
|
|
101
|
+
const callId = generateCallId({
|
|
102
|
+
procedure,
|
|
103
|
+
channel
|
|
104
|
+
});
|
|
105
|
+
return new Promise((resolve) => {
|
|
106
|
+
const timer = setTimeout(() => {
|
|
107
|
+
serverToClientPending.delete(callId);
|
|
108
|
+
resolve({
|
|
109
|
+
success: false,
|
|
110
|
+
error: {
|
|
111
|
+
code: "ERR_TIMEOUT",
|
|
112
|
+
procedure
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
}, timeout);
|
|
116
|
+
serverToClientPending.set(callId, {
|
|
117
|
+
resolve,
|
|
118
|
+
timer
|
|
119
|
+
});
|
|
120
|
+
emitNet(`__rpc_s2c_${channel}__`, playerId, callId, procedure, args[0] ?? null);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
//#endregion
|
|
126
|
+
exports.initializeRpc = initializeRpc;
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registry of client-to-server RPC procedures.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* declare module "fivem-rpc/types" {
|
|
6
|
+
* interface ClientToServerRpc {
|
|
7
|
+
* "player:getData": RpcPayload<undefined, { name: string }>;
|
|
8
|
+
* }
|
|
9
|
+
* }
|
|
10
|
+
*/
|
|
11
|
+
export interface ClientToServerRpc {
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Registry of server-to-client RPC procedures.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* declare module "fivem-rpc/types" {
|
|
18
|
+
* interface ServerToClientRpc {
|
|
19
|
+
* "player:showNotification": RpcPayload<{ message: string }, { seen: boolean }>;
|
|
20
|
+
* }
|
|
21
|
+
* }
|
|
22
|
+
*/
|
|
23
|
+
export interface ServerToClientRpc {
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Registry of NUI-to-client RPC procedures (NUI calls client script).
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* declare module "fivem-rpc/types" {
|
|
30
|
+
* interface NuiToClientRpc {
|
|
31
|
+
* "ui:getPlayerInfo": RpcPayload<undefined, { name: string }>;
|
|
32
|
+
* }
|
|
33
|
+
* }
|
|
34
|
+
*/
|
|
35
|
+
export interface NuiToClientRpc {
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Registry of client-to-NUI RPC procedures (client script calls NUI).
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* declare module "fivem-rpc/types" {
|
|
42
|
+
* interface ClientToNuiRpc {
|
|
43
|
+
* "ui:showNotification": RpcPayload<{ message: string }, { seen: boolean }>;
|
|
44
|
+
* }
|
|
45
|
+
* }
|
|
46
|
+
*/
|
|
47
|
+
export interface ClientToNuiRpc {
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Describes the request and response types for a single RPC procedure.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* "player:setJob": RpcPayload<{ job: string }, { ok: boolean }>
|
|
54
|
+
* "player:ping": RpcPayload<undefined, undefined>
|
|
55
|
+
*/
|
|
56
|
+
export type RpcPayload<Request extends Record<string, unknown> | undefined = undefined, Response extends Record<string, unknown> | undefined = undefined> = {
|
|
57
|
+
request: Request;
|
|
58
|
+
response: Response;
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* Returned by all RPC call functions. Narrow on `success` before accessing `data` or `error`.
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* const result = await callServerRpc("player:getData");
|
|
65
|
+
* if (result.success) console.log(result.data.name);
|
|
66
|
+
* else if (result.error.code === "ERR_TIMEOUT") console.warn("timed out");
|
|
67
|
+
*/
|
|
68
|
+
export type RpcResult<T> = {
|
|
69
|
+
success: true;
|
|
70
|
+
data: T;
|
|
71
|
+
error?: never;
|
|
72
|
+
} | {
|
|
73
|
+
success: false;
|
|
74
|
+
data?: never;
|
|
75
|
+
error: RpcError;
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* Discriminated error union returned in `RpcResult` on failure.
|
|
79
|
+
*
|
|
80
|
+
* - `ERR_NO_HANDLER`: no handler registered for the procedure
|
|
81
|
+
* - `ERR_TIMEOUT`: response not received within the timeout
|
|
82
|
+
* - `ERR_HANDLER`: handler threw at runtime
|
|
83
|
+
*/
|
|
84
|
+
export type RpcError = {
|
|
85
|
+
code: "ERR_NO_HANDLER";
|
|
86
|
+
procedure: string;
|
|
87
|
+
} | {
|
|
88
|
+
code: "ERR_TIMEOUT";
|
|
89
|
+
procedure: string;
|
|
90
|
+
} | {
|
|
91
|
+
code: "ERR_HANDLER";
|
|
92
|
+
message: string;
|
|
93
|
+
};
|
|
94
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,MAAM,WAAW,iBAAiB;CAAG;AAErC;;;;;;;;;GASG;AAEH,MAAM,WAAW,iBAAiB;CAAG;AAErC;;;;;;;;;GASG;AAEH,MAAM,WAAW,cAAc;CAAG;AAElC;;;;;;;;;GASG;AAEH,MAAM,WAAW,cAAc;CAAG;AAElC;;;;;;GAMG;AACH,MAAM,MAAM,UAAU,CACrB,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,GAAG,SAAS,EAC/D,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,GAAG,SAAS,IAC7D;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC;AAE7C;;;;;;;GAOG;AACH,MAAM,MAAM,SAAS,CAAC,CAAC,IACpB;IAAE,OAAO,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,CAAC,CAAC;IAAC,KAAK,CAAC,EAAE,KAAK,CAAA;CAAE,GACzC;IAAE,OAAO,EAAE,KAAK,CAAC;IAAC,IAAI,CAAC,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,QAAQ,CAAA;CAAE,CAAC;AAErD;;;;;;GAMG;AACH,MAAM,MAAM,QAAQ,GACjB;IAAE,IAAI,EAAE,gBAAgB,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GAC7C;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GAC1C;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC"}
|
package/dist/types.js
ADDED
|
File without changes
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,oEAAoE;AACpE,eAAO,MAAM,cAAc,GAAI,yBAG5B;IACF,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;CAChB,KAAG,MASA,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "fivem-rpc",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "A TypeScript library for creating type-safe RPCs in FiveM.",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/carlos-menezes/fivem-rpc"
|
|
9
|
+
},
|
|
10
|
+
"author": "Carlos Menezes",
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"exports": {
|
|
13
|
+
"./types": {
|
|
14
|
+
"types": "./dist/types.d.ts",
|
|
15
|
+
"default": "./dist/types.js"
|
|
16
|
+
},
|
|
17
|
+
"./client": {
|
|
18
|
+
"types": "./dist/client.d.ts",
|
|
19
|
+
"default": "./dist/client.js"
|
|
20
|
+
},
|
|
21
|
+
"./server": {
|
|
22
|
+
"types": "./dist/server.d.ts",
|
|
23
|
+
"default": "./dist/server.js"
|
|
24
|
+
},
|
|
25
|
+
"./nui": {
|
|
26
|
+
"types": "./dist/nui.d.ts",
|
|
27
|
+
"default": "./dist/nui.js"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"dist"
|
|
32
|
+
],
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@biomejs/biome": "2.4.16",
|
|
35
|
+
"@changesets/cli": "^2.31.0",
|
|
36
|
+
"@citizenfx/client": "^2.0.29753-1",
|
|
37
|
+
"@citizenfx/server": "^2.0.29753-1",
|
|
38
|
+
"@commitlint/cli": "^21.0.2",
|
|
39
|
+
"@commitlint/config-conventional": "^21.0.2",
|
|
40
|
+
"happy-dom": "^20.9.0",
|
|
41
|
+
"husky": "^9.1.7",
|
|
42
|
+
"rolldown": "^1.0.3",
|
|
43
|
+
"typescript": "^6.0.3",
|
|
44
|
+
"vitest": "^4.1.7"
|
|
45
|
+
},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"@citizenfx/client": "^2.0.29753-1",
|
|
48
|
+
"@citizenfx/server": "^2.0.29753-1"
|
|
49
|
+
},
|
|
50
|
+
"scripts": {
|
|
51
|
+
"build": "rolldown --config rolldown.config.ts && tsc",
|
|
52
|
+
"test": "vitest run",
|
|
53
|
+
"test:watch": "vitest",
|
|
54
|
+
"format": "biome format --write",
|
|
55
|
+
"lint": "biome lint --write",
|
|
56
|
+
"changeset": "changeset",
|
|
57
|
+
"version": "changeset version",
|
|
58
|
+
"release": "pnpm build && changeset publish"
|
|
59
|
+
}
|
|
60
|
+
}
|