@open-core/framework 1.0.8 → 1.0.10
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 +38 -0
- package/dist/adapters/contracts/IExports.d.ts +38 -0
- package/dist/adapters/contracts/IExports.js +26 -0
- package/dist/adapters/node/node-exports.d.ts +7 -0
- package/dist/adapters/node/node-exports.js +9 -0
- package/dist/runtime/server/bootstrap.js +20 -1
- package/dist/runtime/server/controllers/player-export.controller.d.ts +2 -0
- package/dist/runtime/server/controllers/player-export.controller.js +34 -0
- package/dist/runtime/server/implementations/remote/player.remote.d.ts +4 -0
- package/dist/runtime/server/implementations/remote/player.remote.js +67 -1
- package/dist/runtime/server/types/core-exports.types.d.ts +13 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -141,6 +141,44 @@ export class ExampleNetController {
|
|
|
141
141
|
- `@Throttle(limit, windowMs)`
|
|
142
142
|
- `@RequiresState({ missing: [...] })`
|
|
143
143
|
|
|
144
|
+
### Exports
|
|
145
|
+
|
|
146
|
+
`@Export()` defines a public resource API. Adapters may expose both direct/local access through `getResource()` and an optional explicit async helper layer through `getRemoteResource()` / `waitForRemoteResource()`.
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
import { Controller, Export } from '@open-core/framework/server'
|
|
150
|
+
import { IExports } from '@open-core/framework/contracts/server'
|
|
151
|
+
|
|
152
|
+
@Controller()
|
|
153
|
+
export class DatabaseController {
|
|
154
|
+
@Export('pingDatabase')
|
|
155
|
+
async pingDatabase() {
|
|
156
|
+
return { success: true }
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
interface DatabaseExports {
|
|
161
|
+
pingDatabase(): Promise<{ success: boolean }>
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
class ExampleConsumer {
|
|
165
|
+
constructor(private readonly exportsService: IExports) {}
|
|
166
|
+
|
|
167
|
+
async ping() {
|
|
168
|
+
const database = await this.exportsService.waitForRemoteResource<DatabaseExports>('database', {
|
|
169
|
+
exportName: 'pingDatabase',
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
return database.pingDatabase()
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Guidance:
|
|
178
|
+
|
|
179
|
+
- `getResource()` is for local/synchronous resolution used by framework internals.
|
|
180
|
+
- `waitForRemoteResource()` / `getRemoteResource()` are optional adapter utilities for explicit async resource-to-resource calls.
|
|
181
|
+
|
|
144
182
|
### Library events
|
|
145
183
|
|
|
146
184
|
Use library wrappers to emit domain events and `@OnLibraryEvent()` to observe them.
|
|
@@ -1,4 +1,42 @@
|
|
|
1
1
|
export declare abstract class IExports {
|
|
2
|
+
/**
|
|
3
|
+
* Registers a local export handler for the current resource.
|
|
4
|
+
*
|
|
5
|
+
* @remarks
|
|
6
|
+
* This is called by the framework during metadata processing when it discovers
|
|
7
|
+
* methods decorated with `@Export()`.
|
|
8
|
+
*/
|
|
2
9
|
abstract register(exportName: string, handler: (...args: unknown[]) => unknown): void;
|
|
10
|
+
/**
|
|
11
|
+
* Resolves exports for a resource using the adapter's direct/local mechanism.
|
|
12
|
+
*
|
|
13
|
+
* @remarks
|
|
14
|
+
* Framework internals rely on this method remaining synchronous and side-effect free.
|
|
15
|
+
* Adapters should return `undefined` when the resource is not directly resolvable.
|
|
16
|
+
*/
|
|
3
17
|
abstract getResource<T = unknown>(resourceName: string): T | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* Returns an async proxy for resource exports when the adapter provides a remote helper layer.
|
|
20
|
+
*
|
|
21
|
+
* @remarks
|
|
22
|
+
* This is optional and should not change the semantics of `getResource()`.
|
|
23
|
+
* Consumers should treat methods on the returned proxy as async.
|
|
24
|
+
*/
|
|
25
|
+
getRemoteResource<T = unknown>(_resourceName: string): T;
|
|
26
|
+
/**
|
|
27
|
+
* Calls a single exported method through the adapter's optional remote helper layer.
|
|
28
|
+
*/
|
|
29
|
+
callRemoteExport<TResult = unknown>(_resourceName: string, _exportName: string, ..._args: unknown[]): Promise<TResult>;
|
|
30
|
+
/**
|
|
31
|
+
* Waits until a resource exposes exports compatible with the adapter's remote helper layer.
|
|
32
|
+
*
|
|
33
|
+
* @param _options.exportName Optional export name that must be present before resolving.
|
|
34
|
+
* @param _options.timeoutMs Maximum time to wait before failing.
|
|
35
|
+
* @param _options.intervalMs Polling interval used by adapters that implement polling.
|
|
36
|
+
*/
|
|
37
|
+
waitForRemoteResource<T = unknown>(_resourceName: string, _options?: {
|
|
38
|
+
exportName?: string;
|
|
39
|
+
timeoutMs?: number;
|
|
40
|
+
intervalMs?: number;
|
|
41
|
+
}): Promise<T>;
|
|
4
42
|
}
|
|
@@ -1,2 +1,28 @@
|
|
|
1
1
|
export class IExports {
|
|
2
|
+
/**
|
|
3
|
+
* Returns an async proxy for resource exports when the adapter provides a remote helper layer.
|
|
4
|
+
*
|
|
5
|
+
* @remarks
|
|
6
|
+
* This is optional and should not change the semantics of `getResource()`.
|
|
7
|
+
* Consumers should treat methods on the returned proxy as async.
|
|
8
|
+
*/
|
|
9
|
+
getRemoteResource(_resourceName) {
|
|
10
|
+
throw new Error('[OpenCore] Remote exports are not supported by the active adapter.');
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Calls a single exported method through the adapter's optional remote helper layer.
|
|
14
|
+
*/
|
|
15
|
+
callRemoteExport(_resourceName, _exportName, ..._args) {
|
|
16
|
+
return Promise.reject(new Error('[OpenCore] Remote exports are not supported by the active adapter.'));
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Waits until a resource exposes exports compatible with the adapter's remote helper layer.
|
|
20
|
+
*
|
|
21
|
+
* @param _options.exportName Optional export name that must be present before resolving.
|
|
22
|
+
* @param _options.timeoutMs Maximum time to wait before failing.
|
|
23
|
+
* @param _options.intervalMs Polling interval used by adapters that implement polling.
|
|
24
|
+
*/
|
|
25
|
+
waitForRemoteResource(_resourceName, _options) {
|
|
26
|
+
return Promise.reject(new Error('[OpenCore] Remote exports are not supported by the active adapter.'));
|
|
27
|
+
}
|
|
2
28
|
}
|
|
@@ -7,6 +7,13 @@ export declare class NodeExports implements IExports {
|
|
|
7
7
|
private exports;
|
|
8
8
|
register(exportName: string, handler: (...args: any[]) => any): void;
|
|
9
9
|
getResource(resourceName: string): any;
|
|
10
|
+
getRemoteResource<T = unknown>(_resourceName: string): T;
|
|
11
|
+
callRemoteExport<TResult = unknown>(_resourceName: string, _exportName: string, ..._args: unknown[]): Promise<TResult>;
|
|
12
|
+
waitForRemoteResource<T = unknown>(_resourceName: string, _options?: {
|
|
13
|
+
exportName?: string;
|
|
14
|
+
timeoutMs?: number;
|
|
15
|
+
intervalMs?: number;
|
|
16
|
+
}): Promise<T>;
|
|
10
17
|
/**
|
|
11
18
|
* Get all registered exports as an object
|
|
12
19
|
*/
|
|
@@ -23,6 +23,15 @@ let NodeExports = class NodeExports {
|
|
|
23
23
|
throw new Error(`Cross-resource exports not supported in Node.js runtime. ` +
|
|
24
24
|
`Attempted to access resource: ${resourceName}`);
|
|
25
25
|
}
|
|
26
|
+
getRemoteResource(_resourceName) {
|
|
27
|
+
throw new Error('[OpenCore] Remote exports are not supported in Node.js runtime.');
|
|
28
|
+
}
|
|
29
|
+
callRemoteExport(_resourceName, _exportName, ..._args) {
|
|
30
|
+
return Promise.reject(new Error('[OpenCore] Remote exports are not supported in Node.js runtime.'));
|
|
31
|
+
}
|
|
32
|
+
waitForRemoteResource(_resourceName, _options) {
|
|
33
|
+
return Promise.reject(new Error('[OpenCore] Remote exports are not supported in Node.js runtime.'));
|
|
34
|
+
}
|
|
26
35
|
/**
|
|
27
36
|
* Get all registered exports as an object
|
|
28
37
|
*/
|
|
@@ -278,13 +278,32 @@ async function tryImportAutoLoad() {
|
|
|
278
278
|
await import('./.opencore/autoload.server.controllers');
|
|
279
279
|
}
|
|
280
280
|
catch (err) {
|
|
281
|
-
if (err
|
|
281
|
+
if (isAutoloadModuleNotFound(err)) {
|
|
282
282
|
loggers.bootstrap.warn(`[Bootstrap] No server controllers autoload file found, skipping.`);
|
|
283
283
|
return;
|
|
284
284
|
}
|
|
285
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
286
|
+
loggers.bootstrap.error(`[Bootstrap] Failed to import server controllers autoload file.`, {
|
|
287
|
+
error: message,
|
|
288
|
+
});
|
|
285
289
|
throw err;
|
|
286
290
|
}
|
|
287
291
|
}
|
|
292
|
+
function isAutoloadModuleNotFound(err) {
|
|
293
|
+
if (!err || typeof err !== 'object') {
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
const error = err;
|
|
297
|
+
const message = typeof error.message === 'string' ? error.message : '';
|
|
298
|
+
const requireStack = Array.isArray(error.requireStack) ? error.requireStack : [];
|
|
299
|
+
if (error.code !== 'MODULE_NOT_FOUND' && !message.includes('Cannot find module')) {
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
if (message.includes('autoload.server.controllers')) {
|
|
303
|
+
return true;
|
|
304
|
+
}
|
|
305
|
+
return requireStack.some((entry) => entry.includes('autoload.server.controllers'));
|
|
306
|
+
}
|
|
288
307
|
/**
|
|
289
308
|
* Runs session recovery to restore sessions for players already connected.
|
|
290
309
|
*
|
|
@@ -23,6 +23,8 @@ export declare class PlayerExportController implements InternalPlayerExports {
|
|
|
23
23
|
isPlayerOnline(accountId: string): boolean;
|
|
24
24
|
getPlayerMeta(clientID: number, key: string): Promise<any>;
|
|
25
25
|
setPlayerMeta(clientID: number, key: string, value: any): void;
|
|
26
|
+
linkPlayerAccount(clientID: number, accountID: string): void;
|
|
27
|
+
unlinkPlayerAccount(clientID: number): void;
|
|
26
28
|
getPlayerStates(clientID: number): string[];
|
|
27
29
|
hasPlayerState(clientID: number, state: string): boolean;
|
|
28
30
|
addPlayerState(clientID: number, state: string): void;
|
|
@@ -11,6 +11,7 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
|
11
11
|
return function (target, key) { decorator(target, key, paramIndex); }
|
|
12
12
|
};
|
|
13
13
|
import { inject } from 'tsyringe';
|
|
14
|
+
import { loggers } from '../../../kernel/logger';
|
|
14
15
|
import { Controller } from '../decorators/controller';
|
|
15
16
|
import { Export } from '../decorators/export';
|
|
16
17
|
import { serializeServerPlayerData } from '../adapter/serialization';
|
|
@@ -67,6 +68,27 @@ let PlayerExportController = class PlayerExportController {
|
|
|
67
68
|
setPlayerMeta(clientID, key, value) {
|
|
68
69
|
this.playerService.setMeta(clientID, key, value);
|
|
69
70
|
}
|
|
71
|
+
linkPlayerAccount(clientID, accountID) {
|
|
72
|
+
const player = this.playerService.getByClient(clientID);
|
|
73
|
+
if (!player)
|
|
74
|
+
return;
|
|
75
|
+
player.linkAccount(accountID);
|
|
76
|
+
loggers.session.debug('Remote player account linked in CORE', {
|
|
77
|
+
clientID,
|
|
78
|
+
accountID,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
unlinkPlayerAccount(clientID) {
|
|
82
|
+
const player = this.playerService.getByClient(clientID);
|
|
83
|
+
if (!player)
|
|
84
|
+
return;
|
|
85
|
+
const previousAccountID = player.accountID;
|
|
86
|
+
player.unlinkAccount();
|
|
87
|
+
loggers.session.debug('Remote player account unlinked in CORE', {
|
|
88
|
+
clientID,
|
|
89
|
+
accountID: previousAccountID,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
70
92
|
// ═══════════════════════════════════════════════════════════════
|
|
71
93
|
// State Management
|
|
72
94
|
// ═══════════════════════════════════════════════════════════════
|
|
@@ -141,6 +163,18 @@ __decorate([
|
|
|
141
163
|
__metadata("design:paramtypes", [Number, String, Object]),
|
|
142
164
|
__metadata("design:returntype", void 0)
|
|
143
165
|
], PlayerExportController.prototype, "setPlayerMeta", null);
|
|
166
|
+
__decorate([
|
|
167
|
+
Export(),
|
|
168
|
+
__metadata("design:type", Function),
|
|
169
|
+
__metadata("design:paramtypes", [Number, String]),
|
|
170
|
+
__metadata("design:returntype", void 0)
|
|
171
|
+
], PlayerExportController.prototype, "linkPlayerAccount", null);
|
|
172
|
+
__decorate([
|
|
173
|
+
Export(),
|
|
174
|
+
__metadata("design:type", Function),
|
|
175
|
+
__metadata("design:paramtypes", [Number]),
|
|
176
|
+
__metadata("design:returntype", void 0)
|
|
177
|
+
], PlayerExportController.prototype, "unlinkPlayerAccount", null);
|
|
144
178
|
__decorate([
|
|
145
179
|
Export(),
|
|
146
180
|
__metadata("design:type", Function),
|
|
@@ -35,6 +35,10 @@ export declare class RemotePlayerImplementation extends Players {
|
|
|
35
35
|
*/
|
|
36
36
|
private get core();
|
|
37
37
|
private createPlayerFromData;
|
|
38
|
+
/**
|
|
39
|
+
* Proxies remote session mutations to CORE so security-critical data remains authoritative.
|
|
40
|
+
*/
|
|
41
|
+
private attachAuthoritativeMutators;
|
|
38
42
|
/**
|
|
39
43
|
* Returns a Player instance with real session data from CORE.
|
|
40
44
|
*/
|
|
@@ -77,7 +77,73 @@ let RemotePlayerImplementation = class RemotePlayerImplementation extends Player
|
|
|
77
77
|
return coreExports;
|
|
78
78
|
}
|
|
79
79
|
createPlayerFromData(data) {
|
|
80
|
-
|
|
80
|
+
const player = createRemoteServerPlayer(data, this.playerAdapters);
|
|
81
|
+
this.attachAuthoritativeMutators(player);
|
|
82
|
+
return player;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Proxies remote session mutations to CORE so security-critical data remains authoritative.
|
|
86
|
+
*/
|
|
87
|
+
attachAuthoritativeMutators(player) {
|
|
88
|
+
const core = this.core;
|
|
89
|
+
const originalSetMeta = player.setMeta.bind(player);
|
|
90
|
+
const originalLinkAccount = player.linkAccount.bind(player);
|
|
91
|
+
const originalUnlinkAccount = player.unlinkAccount.bind(player);
|
|
92
|
+
const originalAddState = player.addState.bind(player);
|
|
93
|
+
const originalRemoveState = player.removeState.bind(player);
|
|
94
|
+
const originalToggleState = player.toggleState.bind(player);
|
|
95
|
+
player.setMeta = (key, value) => {
|
|
96
|
+
core.setPlayerMeta(player.clientID, key, value);
|
|
97
|
+
originalSetMeta(key, value);
|
|
98
|
+
loggers.session.debug('Remote player meta delegated to CORE', {
|
|
99
|
+
clientID: player.clientID,
|
|
100
|
+
key,
|
|
101
|
+
});
|
|
102
|
+
};
|
|
103
|
+
player.linkAccount = (accountID) => {
|
|
104
|
+
core.linkPlayerAccount(player.clientID, accountID.toString());
|
|
105
|
+
originalLinkAccount(accountID);
|
|
106
|
+
loggers.session.debug('Remote player linkAccount delegated to CORE', {
|
|
107
|
+
clientID: player.clientID,
|
|
108
|
+
accountID: accountID.toString(),
|
|
109
|
+
});
|
|
110
|
+
};
|
|
111
|
+
player.unlinkAccount = () => {
|
|
112
|
+
const previousAccountID = player.accountID;
|
|
113
|
+
core.unlinkPlayerAccount(player.clientID);
|
|
114
|
+
originalUnlinkAccount();
|
|
115
|
+
loggers.session.debug('Remote player unlinkAccount delegated to CORE', {
|
|
116
|
+
clientID: player.clientID,
|
|
117
|
+
accountID: previousAccountID,
|
|
118
|
+
});
|
|
119
|
+
};
|
|
120
|
+
player.addState = (state) => {
|
|
121
|
+
core.addPlayerState(player.clientID, state);
|
|
122
|
+
originalAddState(state);
|
|
123
|
+
loggers.session.debug('Remote player state added in CORE', {
|
|
124
|
+
clientID: player.clientID,
|
|
125
|
+
state,
|
|
126
|
+
});
|
|
127
|
+
};
|
|
128
|
+
player.removeState = (state) => {
|
|
129
|
+
core.removePlayerState(player.clientID, state);
|
|
130
|
+
originalRemoveState(state);
|
|
131
|
+
loggers.session.debug('Remote player state removed in CORE', {
|
|
132
|
+
clientID: player.clientID,
|
|
133
|
+
state,
|
|
134
|
+
});
|
|
135
|
+
};
|
|
136
|
+
player.toggleState = (state, force) => {
|
|
137
|
+
const next = force ?? !player.hasState(state);
|
|
138
|
+
if (next) {
|
|
139
|
+
player.addState(state);
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
player.removeState(state);
|
|
143
|
+
}
|
|
144
|
+
originalToggleState(state, next);
|
|
145
|
+
return next;
|
|
146
|
+
};
|
|
81
147
|
}
|
|
82
148
|
/**
|
|
83
149
|
* Returns a Player instance with real session data from CORE.
|
|
@@ -225,6 +225,19 @@ export interface InternalPlayerExports {
|
|
|
225
225
|
* @param value - Value to store
|
|
226
226
|
*/
|
|
227
227
|
setPlayerMeta(clientID: number, key: string, value: unknown): void;
|
|
228
|
+
/**
|
|
229
|
+
* Links a persistent account to the authoritative CORE player session.
|
|
230
|
+
*
|
|
231
|
+
* @param clientID - FiveM client/server ID
|
|
232
|
+
* @param accountID - Persistent account identifier
|
|
233
|
+
*/
|
|
234
|
+
linkPlayerAccount(clientID: number, accountID: string): void;
|
|
235
|
+
/**
|
|
236
|
+
* Removes any linked account from the authoritative CORE player session.
|
|
237
|
+
*
|
|
238
|
+
* @param clientID - FiveM client/server ID
|
|
239
|
+
*/
|
|
240
|
+
unlinkPlayerAccount(clientID: number): void;
|
|
228
241
|
/**
|
|
229
242
|
* Gets complete serialized player data.
|
|
230
243
|
*
|