@open-core/ragemp-adapter 1.0.0 → 1.0.2
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/dist/.tsbuildinfo/client.tsbuildinfo +1 -1
- package/dist/.tsbuildinfo/root.tsbuildinfo +1 -1
- package/dist/.tsbuildinfo/server.tsbuildinfo +1 -1
- package/dist/.tsbuildinfo/shared.tsbuildinfo +1 -1
- package/dist/server/ragemp-exports.d.ts +51 -0
- package/dist/server/ragemp-exports.js +202 -1
- package/dist/shared/transport/ragemp.rpc.js +13 -5
- package/package.json +2 -2
|
@@ -1,7 +1,58 @@
|
|
|
1
1
|
import { IExports, IResourceInfo } from '@open-core/framework/contracts/server';
|
|
2
2
|
export declare class RageMPExports extends IExports {
|
|
3
3
|
private readonly resourceInfo;
|
|
4
|
+
private readonly currentResourceName;
|
|
5
|
+
private readonly requestEventName;
|
|
6
|
+
private readonly responseEventName;
|
|
7
|
+
private readonly pendingRequests;
|
|
8
|
+
private readonly localHandlers;
|
|
4
9
|
constructor(resourceInfo: IResourceInfo);
|
|
5
10
|
register(exportName: string, handler: (...args: readonly unknown[]) => unknown): void;
|
|
11
|
+
/**
|
|
12
|
+
* Resolves exports through the local registry shared by the current adapter runtime.
|
|
13
|
+
*/
|
|
6
14
|
getResource<T = unknown>(resourceName: string): T | undefined;
|
|
15
|
+
/**
|
|
16
|
+
* Returns an async proxy that forwards method calls to another server resource.
|
|
17
|
+
*
|
|
18
|
+
* @remarks
|
|
19
|
+
* The proxy intentionally ignores Promise-like properties (`then`, `catch`, `finally`)
|
|
20
|
+
* so `await` does not accidentally treat the proxy itself as a thenable.
|
|
21
|
+
*/
|
|
22
|
+
getRemoteResource<T = unknown>(resourceName: string): T;
|
|
23
|
+
/**
|
|
24
|
+
* Invokes a remote export by sending a server-side RageMP event to the target resource.
|
|
25
|
+
*/
|
|
26
|
+
callRemoteExport<TResult = unknown>(resourceName: string, exportName: string, ...args: unknown[]): Promise<TResult>;
|
|
27
|
+
/**
|
|
28
|
+
* Waits for a remote resource to expose a matching export before returning the async proxy.
|
|
29
|
+
*/
|
|
30
|
+
waitForRemoteResource<T = unknown>(resourceName: string, options?: {
|
|
31
|
+
exportName?: string;
|
|
32
|
+
timeoutMs?: number;
|
|
33
|
+
intervalMs?: number;
|
|
34
|
+
}): Promise<T>;
|
|
35
|
+
/**
|
|
36
|
+
* Installs the request/response listeners used by the optional remote export helper layer.
|
|
37
|
+
*
|
|
38
|
+
* @remarks
|
|
39
|
+
* The listeners reject payloads that look like player-originated server events so clients
|
|
40
|
+
* cannot invoke server resource exports through this transport.
|
|
41
|
+
*/
|
|
42
|
+
private registerTransportListeners;
|
|
43
|
+
/**
|
|
44
|
+
* Executes one remote export request against handlers registered in the current resource.
|
|
45
|
+
*/
|
|
46
|
+
private handleExportRequest;
|
|
47
|
+
/**
|
|
48
|
+
* Resolves or rejects the pending promise associated with a remote export response.
|
|
49
|
+
*/
|
|
50
|
+
private handleExportResponse;
|
|
51
|
+
/**
|
|
52
|
+
* Executes an export from the current resource without going back through the shared registry.
|
|
53
|
+
*/
|
|
54
|
+
private executeLocalExport;
|
|
55
|
+
private createRequestId;
|
|
56
|
+
private serializeError;
|
|
57
|
+
private deserializeError;
|
|
7
58
|
}
|
|
@@ -16,17 +16,218 @@ exports.RageMPExports = void 0;
|
|
|
16
16
|
const tsyringe_1 = require("tsyringe");
|
|
17
17
|
const server_1 = require("@open-core/framework/contracts/server");
|
|
18
18
|
const exports_registry_1 = require("../shared/exports-registry");
|
|
19
|
+
const REQUEST_EVENT_PREFIX = '__oc:exports:req:';
|
|
20
|
+
const RESPONSE_EVENT_PREFIX = '__oc:exports:res:';
|
|
21
|
+
const EXPORTS_TIMEOUT_MS = 7500;
|
|
22
|
+
const WAIT_INTERVAL_MS = 150;
|
|
23
|
+
const META_HAS_EXPORT = '__oc_meta_has_export__';
|
|
24
|
+
function isObjectRecord(value) {
|
|
25
|
+
return typeof value === 'object' && value !== null;
|
|
26
|
+
}
|
|
27
|
+
function isPlayerMpLike(value) {
|
|
28
|
+
return isObjectRecord(value) && 'id' in value && 'serial' in value;
|
|
29
|
+
}
|
|
30
|
+
function isExportRequestPayload(value) {
|
|
31
|
+
return (isObjectRecord(value) &&
|
|
32
|
+
typeof value.requestId === 'string' &&
|
|
33
|
+
typeof value.callerResource === 'string' &&
|
|
34
|
+
typeof value.exportName === 'string' &&
|
|
35
|
+
Array.isArray(value.args));
|
|
36
|
+
}
|
|
37
|
+
function isExportResponsePayload(value) {
|
|
38
|
+
if (!isObjectRecord(value) ||
|
|
39
|
+
typeof value.requestId !== 'string' ||
|
|
40
|
+
typeof value.ok !== 'boolean') {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
if (value.ok) {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
return (isObjectRecord(value.error) &&
|
|
47
|
+
typeof value.error.name === 'string' &&
|
|
48
|
+
typeof value.error.message === 'string');
|
|
49
|
+
}
|
|
19
50
|
let RageMPExports = class RageMPExports extends server_1.IExports {
|
|
20
51
|
constructor(resourceInfo) {
|
|
21
52
|
super();
|
|
22
53
|
this.resourceInfo = resourceInfo;
|
|
54
|
+
this.pendingRequests = new Map();
|
|
55
|
+
this.localHandlers = new Map();
|
|
56
|
+
this.currentResourceName = this.resourceInfo.getCurrentResourceName();
|
|
57
|
+
this.requestEventName = `${REQUEST_EVENT_PREFIX}${this.currentResourceName}`;
|
|
58
|
+
this.responseEventName = `${RESPONSE_EVENT_PREFIX}${this.currentResourceName}`;
|
|
59
|
+
this.registerTransportListeners();
|
|
23
60
|
}
|
|
24
61
|
register(exportName, handler) {
|
|
25
|
-
|
|
62
|
+
this.localHandlers.set(exportName, handler);
|
|
63
|
+
exports_registry_1.exportsRegistry.register(this.currentResourceName, exportName, handler);
|
|
26
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* Resolves exports through the local registry shared by the current adapter runtime.
|
|
67
|
+
*/
|
|
27
68
|
getResource(resourceName) {
|
|
28
69
|
return exports_registry_1.exportsRegistry.resourceProxy(resourceName);
|
|
29
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* Returns an async proxy that forwards method calls to another server resource.
|
|
73
|
+
*
|
|
74
|
+
* @remarks
|
|
75
|
+
* The proxy intentionally ignores Promise-like properties (`then`, `catch`, `finally`)
|
|
76
|
+
* so `await` does not accidentally treat the proxy itself as a thenable.
|
|
77
|
+
*/
|
|
78
|
+
getRemoteResource(resourceName) {
|
|
79
|
+
return new Proxy({}, {
|
|
80
|
+
get: (_, exportName) => {
|
|
81
|
+
if (typeof exportName !== 'string' ||
|
|
82
|
+
exportName === 'then' ||
|
|
83
|
+
exportName === 'catch' ||
|
|
84
|
+
exportName === 'finally') {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
return (...args) => this.callRemoteExport(resourceName, exportName, ...args);
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Invokes a remote export by sending a server-side RageMP event to the target resource.
|
|
93
|
+
*/
|
|
94
|
+
callRemoteExport(resourceName, exportName, ...args) {
|
|
95
|
+
const requestId = this.createRequestId(resourceName, exportName);
|
|
96
|
+
const requestEventName = `${REQUEST_EVENT_PREFIX}${resourceName}`;
|
|
97
|
+
return new Promise((resolve, reject) => {
|
|
98
|
+
const timeout = setTimeout(() => {
|
|
99
|
+
this.pendingRequests.delete(requestId);
|
|
100
|
+
reject(new Error(`[exports] Timed out calling "${exportName}" on resource "${resourceName}".`));
|
|
101
|
+
}, EXPORTS_TIMEOUT_MS);
|
|
102
|
+
this.pendingRequests.set(requestId, { resolve, reject, timeout });
|
|
103
|
+
mp.events.call(requestEventName, {
|
|
104
|
+
requestId,
|
|
105
|
+
callerResource: this.currentResourceName,
|
|
106
|
+
exportName,
|
|
107
|
+
args,
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Waits for a remote resource to expose a matching export before returning the async proxy.
|
|
113
|
+
*/
|
|
114
|
+
async waitForRemoteResource(resourceName, options) {
|
|
115
|
+
const timeoutMs = options?.timeoutMs ?? EXPORTS_TIMEOUT_MS;
|
|
116
|
+
const intervalMs = options?.intervalMs ?? WAIT_INTERVAL_MS;
|
|
117
|
+
const startedAt = Date.now();
|
|
118
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
119
|
+
try {
|
|
120
|
+
const isAvailable = await this.callRemoteExport(resourceName, META_HAS_EXPORT, options?.exportName);
|
|
121
|
+
if (isAvailable) {
|
|
122
|
+
return this.getRemoteResource(resourceName);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
// Ignore transient availability failures while waiting.
|
|
127
|
+
}
|
|
128
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
129
|
+
}
|
|
130
|
+
throw new Error(`[exports] Timed out waiting for resource "${resourceName}" remote exports.`);
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Installs the request/response listeners used by the optional remote export helper layer.
|
|
134
|
+
*
|
|
135
|
+
* @remarks
|
|
136
|
+
* The listeners reject payloads that look like player-originated server events so clients
|
|
137
|
+
* cannot invoke server resource exports through this transport.
|
|
138
|
+
*/
|
|
139
|
+
registerTransportListeners() {
|
|
140
|
+
mp.events.add(this.requestEventName, (...args) => {
|
|
141
|
+
const payload = args[0];
|
|
142
|
+
if (isPlayerMpLike(payload) || !isExportRequestPayload(payload)) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
void this.handleExportRequest(payload);
|
|
146
|
+
});
|
|
147
|
+
mp.events.add(this.responseEventName, (...args) => {
|
|
148
|
+
const payload = args[0];
|
|
149
|
+
if (isPlayerMpLike(payload) || !isExportResponsePayload(payload)) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
this.handleExportResponse(payload);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Executes one remote export request against handlers registered in the current resource.
|
|
157
|
+
*/
|
|
158
|
+
async handleExportRequest(payload) {
|
|
159
|
+
const responseEventName = `${RESPONSE_EVENT_PREFIX}${payload.callerResource}`;
|
|
160
|
+
try {
|
|
161
|
+
const result = await this.executeLocalExport(payload.exportName, payload.args);
|
|
162
|
+
mp.events.call(responseEventName, {
|
|
163
|
+
requestId: payload.requestId,
|
|
164
|
+
ok: true,
|
|
165
|
+
result,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
mp.events.call(responseEventName, {
|
|
170
|
+
requestId: payload.requestId,
|
|
171
|
+
ok: false,
|
|
172
|
+
error: this.serializeError(error),
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Resolves or rejects the pending promise associated with a remote export response.
|
|
178
|
+
*/
|
|
179
|
+
handleExportResponse(payload) {
|
|
180
|
+
const pendingRequest = this.pendingRequests.get(payload.requestId);
|
|
181
|
+
if (!pendingRequest) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
clearTimeout(pendingRequest.timeout);
|
|
185
|
+
this.pendingRequests.delete(payload.requestId);
|
|
186
|
+
if (payload.ok) {
|
|
187
|
+
pendingRequest.resolve(payload.result);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
pendingRequest.reject(this.deserializeError(payload.error));
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Executes an export from the current resource without going back through the shared registry.
|
|
194
|
+
*/
|
|
195
|
+
async executeLocalExport(exportName, args) {
|
|
196
|
+
if (exportName === META_HAS_EXPORT) {
|
|
197
|
+
const requestedExportName = typeof args[0] === 'string' ? args[0] : undefined;
|
|
198
|
+
if (!requestedExportName) {
|
|
199
|
+
return this.localHandlers.size > 0;
|
|
200
|
+
}
|
|
201
|
+
return this.localHandlers.has(requestedExportName);
|
|
202
|
+
}
|
|
203
|
+
const handler = this.localHandlers.get(exportName);
|
|
204
|
+
if (!handler) {
|
|
205
|
+
throw new Error(`[exports] Export "${exportName}" not found in resource "${this.currentResourceName}".`);
|
|
206
|
+
}
|
|
207
|
+
return Promise.resolve(handler(...args));
|
|
208
|
+
}
|
|
209
|
+
createRequestId(resourceName, exportName) {
|
|
210
|
+
return `${this.currentResourceName}:${resourceName}:${exportName}:${Date.now()}:${Math.random()}`;
|
|
211
|
+
}
|
|
212
|
+
serializeError(error) {
|
|
213
|
+
if (error instanceof Error) {
|
|
214
|
+
return {
|
|
215
|
+
name: error.name,
|
|
216
|
+
message: error.message,
|
|
217
|
+
stack: error.stack,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
return {
|
|
221
|
+
name: 'Error',
|
|
222
|
+
message: String(error),
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
deserializeError(error) {
|
|
226
|
+
const result = new Error(error.message);
|
|
227
|
+
result.name = error.name;
|
|
228
|
+
result.stack = error.stack;
|
|
229
|
+
return result;
|
|
230
|
+
}
|
|
30
231
|
};
|
|
31
232
|
exports.RageMPExports = RageMPExports;
|
|
32
233
|
exports.RageMPExports = RageMPExports = __decorate([
|
|
@@ -3,11 +3,19 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.RageMPRpc = void 0;
|
|
4
4
|
const contracts_1 = require("@open-core/framework/contracts");
|
|
5
5
|
const helpers_1 = require("./helpers");
|
|
6
|
-
function
|
|
7
|
-
if (error
|
|
8
|
-
|
|
6
|
+
function serializeRpcError(error) {
|
|
7
|
+
if (typeof error === 'object' &&
|
|
8
|
+
error !== null &&
|
|
9
|
+
'message' in error &&
|
|
10
|
+
typeof error.message === 'string' &&
|
|
11
|
+
'expose' in error &&
|
|
12
|
+
error.expose === true) {
|
|
13
|
+
return {
|
|
14
|
+
message: error.message,
|
|
15
|
+
name: 'name' in error && typeof error.name === 'string' ? error.name : undefined,
|
|
16
|
+
};
|
|
9
17
|
}
|
|
10
|
-
return { message:
|
|
18
|
+
return { message: 'An internal server error occurred' };
|
|
11
19
|
}
|
|
12
20
|
function getCurrentResourceNameSafe() {
|
|
13
21
|
if (typeof __OPENCORE_RESOURCE_NAME__ === 'string' && __OPENCORE_RESOURCE_NAME__.trim()) {
|
|
@@ -148,7 +156,7 @@ class RageMPRpc extends contracts_1.RpcAPI {
|
|
|
148
156
|
}
|
|
149
157
|
}
|
|
150
158
|
catch (err) {
|
|
151
|
-
const errorInfo =
|
|
159
|
+
const errorInfo = serializeRpcError(err);
|
|
152
160
|
if (msg.kind === 'notify') {
|
|
153
161
|
this.emitResponse(replyTarget, { kind: 'ack', id: msg.id });
|
|
154
162
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-core/ragemp-adapter",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "External Rage Multiplayer adapter for OpenCore framework.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"zod": "^4.3.5"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
|
-
"@biomejs/biome": "^2.4.
|
|
46
|
+
"@biomejs/biome": "^2.4.10",
|
|
47
47
|
"@open-core/framework": "latest",
|
|
48
48
|
"@ragempcommunity/types-client": "^2.1.9",
|
|
49
49
|
"@ragempcommunity/types-server": "^2.1.9",
|