@open-core/framework 0.2.7 → 0.2.8
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 +1 -1
- package/dist/adapters/contracts/IEngineEvents.d.ts +1 -1
- package/dist/adapters/contracts/IPlayerServer.d.ts +10 -0
- package/dist/adapters/fivem/fivem-engine-events.d.ts +1 -1
- package/dist/adapters/fivem/fivem-engine-events.js +4 -1
- package/dist/adapters/fivem/fivem-player-server.d.ts +1 -0
- package/dist/adapters/fivem/fivem-player-server.js +3 -0
- package/dist/adapters/node/node-engine-events.d.ts +1 -1
- package/dist/adapters/node/node-engine-events.js +4 -1
- package/dist/adapters/node/node-player-server.d.ts +1 -0
- package/dist/adapters/node/node-player-server.js +3 -0
- package/dist/runtime/server/bootstrap.js +99 -2
- package/dist/runtime/server/decorators/command.d.ts +1 -0
- package/dist/runtime/server/runtime.d.ts +34 -1
- package/dist/runtime/server/runtime.js +1 -0
- package/dist/runtime/server/services/core/session-recovery.service.d.ts +39 -0
- package/dist/runtime/server/services/core/session-recovery.service.js +108 -0
- package/dist/runtime/server/services/index.d.ts +1 -0
- package/dist/runtime/server/services/index.js +3 -1
- package/dist/runtime/server/services/services.register.js +2 -0
- package/dist/runtime/server/types/internal-events.d.ts +6 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ export declare abstract class IEngineEvents {
|
|
|
5
5
|
* @param eventName - The event name to listen for
|
|
6
6
|
* @param handler - The callback to invoke when the event is emitted
|
|
7
7
|
*/
|
|
8
|
-
abstract on(eventName: string, handler
|
|
8
|
+
abstract on(eventName: string, handler?: (...args: any[]) => void): void;
|
|
9
9
|
/**
|
|
10
10
|
* Emits a local (server-side) event.
|
|
11
11
|
*
|
|
@@ -70,4 +70,14 @@ export declare abstract class IPlayerServer {
|
|
|
70
70
|
* @param bucket - Routing bucket ID
|
|
71
71
|
*/
|
|
72
72
|
abstract setRoutingBucket(playerSrc: string, bucket: number): void;
|
|
73
|
+
/**
|
|
74
|
+
* Gets all currently connected player sources.
|
|
75
|
+
*
|
|
76
|
+
* @remarks
|
|
77
|
+
* Returns the source IDs (as strings) of all players currently connected to the server.
|
|
78
|
+
* Used for session recovery after resource restarts.
|
|
79
|
+
*
|
|
80
|
+
* @returns Array of player source strings
|
|
81
|
+
*/
|
|
82
|
+
abstract getConnectedPlayers(): string[];
|
|
73
83
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { IEngineEvents } from '../contracts/IEngineEvents';
|
|
2
2
|
export declare class FiveMEngineEvents extends IEngineEvents {
|
|
3
|
-
on(eventName: string, handler
|
|
3
|
+
on(eventName: string, handler?: (...args: any[]) => void): void;
|
|
4
4
|
emit(eventName: string, ...args: any[]): void;
|
|
5
5
|
}
|
|
@@ -4,7 +4,10 @@ exports.FiveMEngineEvents = void 0;
|
|
|
4
4
|
const IEngineEvents_1 = require("../contracts/IEngineEvents");
|
|
5
5
|
class FiveMEngineEvents extends IEngineEvents_1.IEngineEvents {
|
|
6
6
|
on(eventName, handler) {
|
|
7
|
-
|
|
7
|
+
if (handler)
|
|
8
|
+
on(eventName, handler);
|
|
9
|
+
else
|
|
10
|
+
on(eventName, () => { }); // empty handler
|
|
8
11
|
}
|
|
9
12
|
emit(eventName, ...args) {
|
|
10
13
|
emit(eventName, ...args);
|
|
@@ -56,6 +56,9 @@ let FiveMPlayerServer = class FiveMPlayerServer extends IPlayerServer_1.IPlayerS
|
|
|
56
56
|
setRoutingBucket(playerSrc, bucket) {
|
|
57
57
|
SetPlayerRoutingBucket(playerSrc, bucket);
|
|
58
58
|
}
|
|
59
|
+
getConnectedPlayers() {
|
|
60
|
+
return getPlayers();
|
|
61
|
+
}
|
|
59
62
|
};
|
|
60
63
|
exports.FiveMPlayerServer = FiveMPlayerServer;
|
|
61
64
|
exports.FiveMPlayerServer = FiveMPlayerServer = __decorate([
|
|
@@ -8,7 +8,7 @@ import { IEngineEvents } from '../contracts/IEngineEvents';
|
|
|
8
8
|
*/
|
|
9
9
|
export declare class NodeEngineEvents implements IEngineEvents {
|
|
10
10
|
private eventEmitter;
|
|
11
|
-
on(eventName: string, handler
|
|
11
|
+
on(eventName: string, handler?: (...args: any[]) => void): void;
|
|
12
12
|
/**
|
|
13
13
|
* Utility method for testing: emit an engine event
|
|
14
14
|
*/
|
|
@@ -21,7 +21,10 @@ let NodeEngineEvents = class NodeEngineEvents {
|
|
|
21
21
|
this.eventEmitter = new node_events_1.EventEmitter();
|
|
22
22
|
}
|
|
23
23
|
on(eventName, handler) {
|
|
24
|
-
|
|
24
|
+
if (handler)
|
|
25
|
+
this.eventEmitter.on(eventName, handler);
|
|
26
|
+
else
|
|
27
|
+
this.eventEmitter.on(eventName, () => { }); // empty handler
|
|
25
28
|
}
|
|
26
29
|
/**
|
|
27
30
|
* Utility method for testing: emit an engine event
|
|
@@ -71,6 +71,9 @@ let NodePlayerServer = class NodePlayerServer extends IPlayerServer_1.IPlayerSer
|
|
|
71
71
|
setRoutingBucket(_playerSrc, _bucket) {
|
|
72
72
|
// Mock: no-op in Node.js
|
|
73
73
|
}
|
|
74
|
+
getConnectedPlayers() {
|
|
75
|
+
return Array.from(this.players.keys());
|
|
76
|
+
}
|
|
74
77
|
_clear() {
|
|
75
78
|
this.players.clear();
|
|
76
79
|
this.droppedPlayers = [];
|
|
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.initServer = initServer;
|
|
37
|
+
const adapters_1 = require("../../adapters");
|
|
37
38
|
const register_capabilities_1 = require("../../adapters/register-capabilities");
|
|
38
39
|
const index_1 = require("../../kernel/di/index");
|
|
39
40
|
const logger_1 = require("../../kernel/shared/logger");
|
|
@@ -41,8 +42,10 @@ const bootstrap_validation_1 = require("./bootstrap.validation");
|
|
|
41
42
|
const index_2 = require("./contracts/index");
|
|
42
43
|
const controller_1 = require("./decorators/controller");
|
|
43
44
|
const runtime_1 = require("./runtime");
|
|
45
|
+
const session_recovery_service_1 = require("./services/core/session-recovery.service");
|
|
44
46
|
const services_register_1 = require("./services/services.register");
|
|
45
47
|
const processors_register_1 = require("./system/processors.register");
|
|
48
|
+
const CORE_WAIT_TIMEOUT = 10000;
|
|
46
49
|
function checkProviders(ctx) {
|
|
47
50
|
if (ctx.mode === 'RESOURCE')
|
|
48
51
|
return;
|
|
@@ -108,7 +111,7 @@ async function loadFrameworkControllers(ctx) {
|
|
|
108
111
|
* @returns A promise that resolves when the Core is fully initialized and ready to process events.
|
|
109
112
|
*/
|
|
110
113
|
async function initServer(options) {
|
|
111
|
-
var _a;
|
|
114
|
+
var _a, _b, _c, _d;
|
|
112
115
|
(0, runtime_1.validateRuntimeOptions)(options);
|
|
113
116
|
(0, runtime_1.setRuntimeContext)(options);
|
|
114
117
|
const ctx = options;
|
|
@@ -117,7 +120,26 @@ async function initServer(options) {
|
|
|
117
120
|
mode: ctx.mode,
|
|
118
121
|
scope: (0, runtime_1.getFrameworkModeScope)(ctx.mode),
|
|
119
122
|
});
|
|
123
|
+
// Adapters
|
|
120
124
|
await (0, register_capabilities_1.registerServerCapabilities)();
|
|
125
|
+
const dependenciesToWaitFor = [];
|
|
126
|
+
if (ctx.mode === 'RESOURCE') {
|
|
127
|
+
logger_1.loggers.bootstrap.info(`[WAIT] Standing by for Core '${ctx.coreResourceName}' to be ready...`);
|
|
128
|
+
dependenciesToWaitFor.push(createCoreDependency(ctx.coreResourceName));
|
|
129
|
+
}
|
|
130
|
+
if ((_a = options.onDependency) === null || _a === void 0 ? void 0 : _a.waitFor) {
|
|
131
|
+
const userDeps = Array.isArray(options.onDependency.waitFor)
|
|
132
|
+
? options.onDependency.waitFor
|
|
133
|
+
: [options.onDependency.waitFor];
|
|
134
|
+
dependenciesToWaitFor.push(...userDeps);
|
|
135
|
+
}
|
|
136
|
+
if (dependenciesToWaitFor.length > 0 || ((_b = options.onDependency) === null || _b === void 0 ? void 0 : _b.onReady)) {
|
|
137
|
+
await dependencyResolver(dependenciesToWaitFor, (_c = options.onDependency) === null || _c === void 0 ? void 0 : _c.onReady);
|
|
138
|
+
}
|
|
139
|
+
if (ctx.mode === 'RESOURCE') {
|
|
140
|
+
logger_1.loggers.bootstrap.info(`Core ready detected!`);
|
|
141
|
+
}
|
|
142
|
+
logger_1.loggers.bootstrap.debug('Dependencies resolved. Proceeding with system boot.');
|
|
121
143
|
(0, services_register_1.registerServicesServer)(ctx);
|
|
122
144
|
logger_1.loggers.bootstrap.debug('Core services registered');
|
|
123
145
|
(0, processors_register_1.registerSystemServer)(ctx);
|
|
@@ -203,10 +225,85 @@ async function initServer(options) {
|
|
|
203
225
|
const scanner = index_1.di.resolve(index_1.MetadataScanner);
|
|
204
226
|
scanner.scan((0, controller_1.getServerControllerRegistry)());
|
|
205
227
|
// Initialize DevMode if enabled
|
|
206
|
-
if ((
|
|
228
|
+
if ((_d = ctx.devMode) === null || _d === void 0 ? void 0 : _d.enabled) {
|
|
207
229
|
await initDevMode(ctx.devMode);
|
|
208
230
|
}
|
|
231
|
+
// Run session recovery if enabled (recovers sessions for players already connected)
|
|
232
|
+
if (ctx.features.sessionLifecycle.enabled && ctx.features.sessionLifecycle.recoveryOnRestart) {
|
|
233
|
+
runSessionRecovery();
|
|
234
|
+
}
|
|
209
235
|
logger_1.loggers.bootstrap.info('OpenCore Server initialized successfully');
|
|
236
|
+
if (ctx.mode === 'CORE' && index_1.di.isRegistered(adapters_1.IEngineEvents)) {
|
|
237
|
+
const engineInterface = index_1.di.resolve(adapters_1.IEngineEvents);
|
|
238
|
+
engineInterface.emit('core:ready');
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
function createCoreDependency(coreName) {
|
|
242
|
+
return new Promise((resolve, reject) => {
|
|
243
|
+
let resolved = false;
|
|
244
|
+
const engineEvents = index_1.di.resolve(adapters_1.IEngineEvents);
|
|
245
|
+
const cleanup = () => {
|
|
246
|
+
resolved = true;
|
|
247
|
+
clearTimeout(timeout);
|
|
248
|
+
};
|
|
249
|
+
const timeout = setTimeout(() => {
|
|
250
|
+
if (!resolved) {
|
|
251
|
+
reject(new Error(`[OpenCore] Timeout waiting for CORE '${coreName}'. The Core did not emit 'core:ready' within ${CORE_WAIT_TIMEOUT}ms.`));
|
|
252
|
+
}
|
|
253
|
+
}, CORE_WAIT_TIMEOUT);
|
|
254
|
+
const onReady = () => {
|
|
255
|
+
if (!resolved) {
|
|
256
|
+
cleanup();
|
|
257
|
+
resolve();
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
engineEvents.on('core:ready', onReady);
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
async function dependencyResolver(waitFor, onReady) {
|
|
264
|
+
if (waitFor) {
|
|
265
|
+
const dependencyPromises = Array.isArray(waitFor) ? waitFor : [waitFor];
|
|
266
|
+
try {
|
|
267
|
+
await Promise.all(dependencyPromises);
|
|
268
|
+
}
|
|
269
|
+
catch (err) {
|
|
270
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
271
|
+
logger_1.loggers.bootstrap.fatal(`Failed to resolve startup dependencies`, { error: msg });
|
|
272
|
+
throw new Error(`[OpenCore] Startup aborted: ${msg}`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (onReady) {
|
|
276
|
+
try {
|
|
277
|
+
await onReady();
|
|
278
|
+
}
|
|
279
|
+
catch (err) {
|
|
280
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
281
|
+
logger_1.loggers.bootstrap.fatal('Failed to execute onReady hook', { error: msg });
|
|
282
|
+
throw new Error(`[OpenCore] onReady hook failed: ${msg}`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Runs session recovery to restore sessions for players already connected.
|
|
288
|
+
*
|
|
289
|
+
* @remarks
|
|
290
|
+
* This is useful during development when hot-reloading resources.
|
|
291
|
+
* Players remain connected to FiveM but lose their sessions when the resource restarts.
|
|
292
|
+
* This function detects these orphaned players and recreates their sessions.
|
|
293
|
+
*/
|
|
294
|
+
function runSessionRecovery() {
|
|
295
|
+
try {
|
|
296
|
+
const recoveryService = index_1.di.resolve(session_recovery_service_1.SessionRecoveryService);
|
|
297
|
+
const stats = recoveryService.recoverSessions();
|
|
298
|
+
if (stats.recovered > 0) {
|
|
299
|
+
logger_1.loggers.bootstrap.info(`[SessionRecovery] Recovered ${stats.recovered} player session(s)`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
catch (error) {
|
|
303
|
+
logger_1.loggers.bootstrap.warn('[SessionRecovery] Failed to run session recovery', {
|
|
304
|
+
error: error.message,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
210
307
|
}
|
|
211
308
|
/**
|
|
212
309
|
* Initializes the DevMode subsystem.
|
|
@@ -81,4 +81,5 @@ type ServerCommandHandler = (() => any) | ((player: Player, ...args: any[]) => a
|
|
|
81
81
|
export declare function Command(name: string, schema: z.ZodType): <T extends ServerCommandHandler>(target: any, propertyKey: string, descriptor: TypedPropertyDescriptor<T>) => void;
|
|
82
82
|
export declare function Command(name: string): <T extends ServerCommandHandler>(target: any, propertyKey: string, descriptor: TypedPropertyDescriptor<T>) => void;
|
|
83
83
|
export declare function Command(config: CommandConfig): <T extends ServerCommandHandler>(target: any, propertyKey: string, descriptor: TypedPropertyDescriptor<T>) => void;
|
|
84
|
+
export declare function Command(config: CommandConfig, schema: z.ZodType): <T extends ServerCommandHandler>(target: any, propertyKey: string, descriptor: TypedPropertyDescriptor<T>) => void;
|
|
84
85
|
export {};
|
|
@@ -240,8 +240,24 @@ export interface UserFeatureConfig {
|
|
|
240
240
|
*
|
|
241
241
|
* @defaultValue
|
|
242
242
|
* - enabled: true (CORE/STANDALONE), false (RESOURCE)
|
|
243
|
+
* - recoveryOnRestart: true
|
|
243
244
|
*/
|
|
244
|
-
sessionLifecycle?: BaseFeatureConfig
|
|
245
|
+
sessionLifecycle?: BaseFeatureConfig & {
|
|
246
|
+
/**
|
|
247
|
+
* Automatically recover sessions for players already connected when the resource restarts.
|
|
248
|
+
*
|
|
249
|
+
* @remarks
|
|
250
|
+
* When enabled, the framework will scan for connected players on startup and
|
|
251
|
+
* create sessions for any players that don't have an active session.
|
|
252
|
+
* This is useful during development when hot-reloading resources.
|
|
253
|
+
*
|
|
254
|
+
* **Note**: Only basic session data (clientID, identifiers) is recovered.
|
|
255
|
+
* Players will need to re-authenticate to restore accountID and other auth-related data.
|
|
256
|
+
*
|
|
257
|
+
* @defaultValue true
|
|
258
|
+
*/
|
|
259
|
+
recoveryOnRestart?: boolean;
|
|
260
|
+
};
|
|
245
261
|
}
|
|
246
262
|
/**
|
|
247
263
|
* Internal feature contract with all fields resolved.
|
|
@@ -285,6 +301,16 @@ export interface FeatureContract {
|
|
|
285
301
|
* When `true`, the feature cannot be disabled (`enabled` must be `true`).
|
|
286
302
|
*/
|
|
287
303
|
required: boolean;
|
|
304
|
+
/**
|
|
305
|
+
* (sessionLifecycle only) Enable automatic session recovery on resource restart.
|
|
306
|
+
*
|
|
307
|
+
* @remarks
|
|
308
|
+
* When true, scans for connected players on startup and creates sessions
|
|
309
|
+
* for any that don't have an active session.
|
|
310
|
+
*
|
|
311
|
+
* @defaultValue true
|
|
312
|
+
*/
|
|
313
|
+
recoveryOnRestart?: boolean;
|
|
288
314
|
}
|
|
289
315
|
export type FrameworkFeatures = Record<FeatureName, FeatureContract>;
|
|
290
316
|
export interface ResourceGrants {
|
|
@@ -318,6 +344,10 @@ export interface DevModeConfig {
|
|
|
318
344
|
autoConnectPlayers: number;
|
|
319
345
|
};
|
|
320
346
|
}
|
|
347
|
+
export interface Hooks {
|
|
348
|
+
waitFor?: Promise<any> | Promise<any>[];
|
|
349
|
+
onReady?: () => Promise<void> | void;
|
|
350
|
+
}
|
|
321
351
|
export interface ServerRuntimeOptions {
|
|
322
352
|
mode: FrameworkMode;
|
|
323
353
|
features: FrameworkFeatures;
|
|
@@ -325,6 +355,7 @@ export interface ServerRuntimeOptions {
|
|
|
325
355
|
resourceGrants?: ResourceGrants;
|
|
326
356
|
/** Development mode configuration (disabled in production) */
|
|
327
357
|
devMode?: DevModeConfig;
|
|
358
|
+
onDependency?: Hooks;
|
|
328
359
|
}
|
|
329
360
|
export type RuntimeContext = ServerRuntimeOptions;
|
|
330
361
|
export declare function setRuntimeContext(ctx: RuntimeContext): void;
|
|
@@ -399,6 +430,8 @@ export interface ServerInitOptions {
|
|
|
399
430
|
resourceGrants?: ResourceGrants;
|
|
400
431
|
/** Development mode configuration (disabled in production) */
|
|
401
432
|
devMode?: DevModeConfig;
|
|
433
|
+
/** If you want to wait for a dependency promise, or when ready do something (By default, the core server will emit a "ready" when it is ready to all resources.) */
|
|
434
|
+
onDependency?: Hooks;
|
|
402
435
|
}
|
|
403
436
|
export declare function resolveRuntimeOptions(options: ServerInitOptions): ServerRuntimeOptions;
|
|
404
437
|
export declare function validateRuntimeOptions(options: ServerRuntimeOptions): void;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { IPlayerServer } from '../../../../adapters/contracts/IPlayerServer';
|
|
2
|
+
import { PlayerDirectoryPort } from '../ports/player-directory.port';
|
|
3
|
+
import { PlayerSessionLifecyclePort } from '../ports/player-session-lifecycle.port';
|
|
4
|
+
/**
|
|
5
|
+
* Service responsible for recovering player sessions after resource restarts.
|
|
6
|
+
*
|
|
7
|
+
* @remarks
|
|
8
|
+
* When a resource restarts (e.g., during development hot-reload), player sessions
|
|
9
|
+
* are lost because the PlayerService map is cleared. However, players remain connected
|
|
10
|
+
* to FiveM. This service detects these orphaned players and recreates their sessions.
|
|
11
|
+
*
|
|
12
|
+
* **Recovery Flow:**
|
|
13
|
+
* 1. Query FiveM for all connected player sources via `getPlayers()`
|
|
14
|
+
* 2. For each connected player, check if a session exists in PlayerService
|
|
15
|
+
* 3. If no session exists, create one with basic identifiers (license, steam, discord)
|
|
16
|
+
* 4. Emit `internal:playerSessionRecovered` event for each recovered session
|
|
17
|
+
*
|
|
18
|
+
* **Limitations:**
|
|
19
|
+
* - Only basic session data is recovered (clientID, identifiers)
|
|
20
|
+
* - `accountID` is NOT recovered - players must re-authenticate
|
|
21
|
+
* - Session metadata and states are NOT recovered
|
|
22
|
+
*/
|
|
23
|
+
export declare class SessionRecoveryService {
|
|
24
|
+
private readonly playerServer;
|
|
25
|
+
private readonly playerDirectory;
|
|
26
|
+
private readonly playerSessionLifecycle;
|
|
27
|
+
constructor(playerServer: IPlayerServer, playerDirectory: PlayerDirectoryPort, playerSessionLifecycle: PlayerSessionLifecyclePort);
|
|
28
|
+
/**
|
|
29
|
+
* Scans for connected players and recovers sessions for any without an active session.
|
|
30
|
+
*
|
|
31
|
+
* @returns Object containing recovery statistics
|
|
32
|
+
*/
|
|
33
|
+
recoverSessions(): {
|
|
34
|
+
total: number;
|
|
35
|
+
recovered: number;
|
|
36
|
+
existing: number;
|
|
37
|
+
};
|
|
38
|
+
private recoverPlayerSession;
|
|
39
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.SessionRecoveryService = void 0;
|
|
13
|
+
const tsyringe_1 = require("tsyringe");
|
|
14
|
+
const IPlayerServer_1 = require("../../../../adapters/contracts/IPlayerServer");
|
|
15
|
+
const logger_1 = require("../../../../kernel/shared/logger");
|
|
16
|
+
const internal_event_bus_1 = require("../../bus/internal-event.bus");
|
|
17
|
+
const player_directory_port_1 = require("../ports/player-directory.port");
|
|
18
|
+
const player_session_lifecycle_port_1 = require("../ports/player-session-lifecycle.port");
|
|
19
|
+
/**
|
|
20
|
+
* Service responsible for recovering player sessions after resource restarts.
|
|
21
|
+
*
|
|
22
|
+
* @remarks
|
|
23
|
+
* When a resource restarts (e.g., during development hot-reload), player sessions
|
|
24
|
+
* are lost because the PlayerService map is cleared. However, players remain connected
|
|
25
|
+
* to FiveM. This service detects these orphaned players and recreates their sessions.
|
|
26
|
+
*
|
|
27
|
+
* **Recovery Flow:**
|
|
28
|
+
* 1. Query FiveM for all connected player sources via `getPlayers()`
|
|
29
|
+
* 2. For each connected player, check if a session exists in PlayerService
|
|
30
|
+
* 3. If no session exists, create one with basic identifiers (license, steam, discord)
|
|
31
|
+
* 4. Emit `internal:playerSessionRecovered` event for each recovered session
|
|
32
|
+
*
|
|
33
|
+
* **Limitations:**
|
|
34
|
+
* - Only basic session data is recovered (clientID, identifiers)
|
|
35
|
+
* - `accountID` is NOT recovered - players must re-authenticate
|
|
36
|
+
* - Session metadata and states are NOT recovered
|
|
37
|
+
*/
|
|
38
|
+
let SessionRecoveryService = class SessionRecoveryService {
|
|
39
|
+
constructor(playerServer, playerDirectory, playerSessionLifecycle) {
|
|
40
|
+
this.playerServer = playerServer;
|
|
41
|
+
this.playerDirectory = playerDirectory;
|
|
42
|
+
this.playerSessionLifecycle = playerSessionLifecycle;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Scans for connected players and recovers sessions for any without an active session.
|
|
46
|
+
*
|
|
47
|
+
* @returns Object containing recovery statistics
|
|
48
|
+
*/
|
|
49
|
+
recoverSessions() {
|
|
50
|
+
const connectedPlayers = this.playerServer.getConnectedPlayers();
|
|
51
|
+
const stats = { total: connectedPlayers.length, recovered: 0, existing: 0 };
|
|
52
|
+
if (connectedPlayers.length === 0) {
|
|
53
|
+
logger_1.loggers.session.debug('[SessionRecovery] No connected players found');
|
|
54
|
+
return stats;
|
|
55
|
+
}
|
|
56
|
+
logger_1.loggers.session.info(`[SessionRecovery] Found ${connectedPlayers.length} connected player(s), checking sessions...`);
|
|
57
|
+
for (const playerSrc of connectedPlayers) {
|
|
58
|
+
const clientId = Number(playerSrc);
|
|
59
|
+
if (Number.isNaN(clientId) || clientId <= 0) {
|
|
60
|
+
logger_1.loggers.session.warn(`[SessionRecovery] Invalid player source: ${playerSrc}`);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const existingPlayer = this.playerDirectory.getByClient(clientId);
|
|
64
|
+
if (existingPlayer) {
|
|
65
|
+
stats.existing++;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
this.recoverPlayerSession(clientId);
|
|
69
|
+
stats.recovered++;
|
|
70
|
+
}
|
|
71
|
+
if (stats.recovered > 0) {
|
|
72
|
+
logger_1.loggers.session.info(`[SessionRecovery] Recovery complete: ${stats.recovered} recovered, ${stats.existing} already existed`);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
logger_1.loggers.session.debug(`[SessionRecovery] All ${stats.existing} sessions already exist`);
|
|
76
|
+
}
|
|
77
|
+
return stats;
|
|
78
|
+
}
|
|
79
|
+
recoverPlayerSession(clientId) {
|
|
80
|
+
const clientIdStr = clientId.toString();
|
|
81
|
+
const license = this.playerServer.getIdentifier(clientIdStr, 'license');
|
|
82
|
+
const steam = this.playerServer.getIdentifier(clientIdStr, 'steam');
|
|
83
|
+
const discord = this.playerServer.getIdentifier(clientIdStr, 'discord');
|
|
84
|
+
const playerName = this.playerServer.getName(clientIdStr);
|
|
85
|
+
const player = this.playerSessionLifecycle.bind(clientId, {
|
|
86
|
+
license,
|
|
87
|
+
steam,
|
|
88
|
+
discord,
|
|
89
|
+
});
|
|
90
|
+
logger_1.loggers.session.info(`[SessionRecovery] Recovered session for player`, {
|
|
91
|
+
clientId,
|
|
92
|
+
name: playerName,
|
|
93
|
+
license: license ? `${license.substring(0, 20)}...` : 'none',
|
|
94
|
+
});
|
|
95
|
+
(0, internal_event_bus_1.emitFrameworkEvent)('internal:playerSessionRecovered', {
|
|
96
|
+
clientId,
|
|
97
|
+
player,
|
|
98
|
+
license,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
exports.SessionRecoveryService = SessionRecoveryService;
|
|
103
|
+
exports.SessionRecoveryService = SessionRecoveryService = __decorate([
|
|
104
|
+
(0, tsyringe_1.injectable)(),
|
|
105
|
+
__metadata("design:paramtypes", [IPlayerServer_1.IPlayerServer,
|
|
106
|
+
player_directory_port_1.PlayerDirectoryPort,
|
|
107
|
+
player_session_lifecycle_port_1.PlayerSessionLifecyclePort])
|
|
108
|
+
], SessionRecoveryService);
|
|
@@ -2,6 +2,7 @@ export * from '../database';
|
|
|
2
2
|
export * from './appearance.service';
|
|
3
3
|
export { ChatService } from './chat.service';
|
|
4
4
|
export { ConfigService } from './config.service';
|
|
5
|
+
export { SessionRecoveryService } from './core/session-recovery.service';
|
|
5
6
|
export { type HttpOptions, HttpService } from './http/http.service';
|
|
6
7
|
export * from './parallel';
|
|
7
8
|
export { PlayerPersistenceService } from './persistence.service';
|
|
@@ -15,13 +15,15 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
15
15
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
16
16
|
};
|
|
17
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
-
exports.RateLimiterService = exports.PlayerPersistenceService = exports.HttpService = exports.ConfigService = exports.ChatService = void 0;
|
|
18
|
+
exports.RateLimiterService = exports.PlayerPersistenceService = exports.HttpService = exports.SessionRecoveryService = exports.ConfigService = exports.ChatService = void 0;
|
|
19
19
|
__exportStar(require("../database"), exports);
|
|
20
20
|
__exportStar(require("./appearance.service"), exports);
|
|
21
21
|
var chat_service_1 = require("./chat.service");
|
|
22
22
|
Object.defineProperty(exports, "ChatService", { enumerable: true, get: function () { return chat_service_1.ChatService; } });
|
|
23
23
|
var config_service_1 = require("./config.service");
|
|
24
24
|
Object.defineProperty(exports, "ConfigService", { enumerable: true, get: function () { return config_service_1.ConfigService; } });
|
|
25
|
+
var session_recovery_service_1 = require("./core/session-recovery.service");
|
|
26
|
+
Object.defineProperty(exports, "SessionRecoveryService", { enumerable: true, get: function () { return session_recovery_service_1.SessionRecoveryService; } });
|
|
25
27
|
var http_service_1 = require("./http/http.service");
|
|
26
28
|
Object.defineProperty(exports, "HttpService", { enumerable: true, get: function () { return http_service_1.HttpService; } });
|
|
27
29
|
__exportStar(require("./parallel"), exports);
|
|
@@ -7,6 +7,7 @@ const chat_service_1 = require("./chat.service");
|
|
|
7
7
|
const command_service_1 = require("./core/command.service");
|
|
8
8
|
const player_service_1 = require("./core/player.service");
|
|
9
9
|
const principal_service_1 = require("./core/principal.service");
|
|
10
|
+
const session_recovery_service_1 = require("./core/session-recovery.service");
|
|
10
11
|
const http_service_1 = require("./http/http.service");
|
|
11
12
|
const persistence_service_1 = require("./persistence.service");
|
|
12
13
|
const command_execution_port_1 = require("./ports/command-execution.port");
|
|
@@ -60,6 +61,7 @@ function registerServicesServer(ctx) {
|
|
|
60
61
|
}
|
|
61
62
|
if (features.sessionLifecycle.enabled && mode !== 'RESOURCE') {
|
|
62
63
|
index_1.di.registerSingleton(persistence_service_1.PlayerPersistenceService, persistence_service_1.PlayerPersistenceService);
|
|
64
|
+
index_1.di.registerSingleton(session_recovery_service_1.SessionRecoveryService, session_recovery_service_1.SessionRecoveryService);
|
|
63
65
|
}
|
|
64
66
|
if (features.principal.enabled) {
|
|
65
67
|
if (features.principal.provider === 'local' || mode === 'CORE' || mode === 'STANDALONE') {
|
|
@@ -14,9 +14,15 @@ export interface TransferCompletedPayload {
|
|
|
14
14
|
export interface PlayerFullyConnectedPayload {
|
|
15
15
|
player: Player;
|
|
16
16
|
}
|
|
17
|
+
export interface PlayerSessionRecoveredPayload {
|
|
18
|
+
clientId: number;
|
|
19
|
+
player: Player;
|
|
20
|
+
license: string | undefined;
|
|
21
|
+
}
|
|
17
22
|
export type InternalEventMap = {
|
|
18
23
|
'internal:playerSessionCreated': PlayerSessionCreatedPayload;
|
|
19
24
|
'internal:playerSessionDestroyed': PlayerSessionDestroyedPayload;
|
|
20
25
|
'internal:transfer:completed': TransferCompletedPayload;
|
|
21
26
|
'internal:playerFullyConnected': PlayerFullyConnectedPayload;
|
|
27
|
+
'internal:playerSessionRecovered': PlayerSessionRecoveredPayload;
|
|
22
28
|
};
|