@open-core/framework 0.2.7 → 0.2.9

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.
Files changed (37) hide show
  1. package/README.md +11 -1
  2. package/dist/adapters/contracts/IEngineEvents.d.ts +1 -1
  3. package/dist/adapters/contracts/IPlayerServer.d.ts +10 -0
  4. package/dist/adapters/fivem/fivem-engine-events.d.ts +1 -1
  5. package/dist/adapters/fivem/fivem-engine-events.js +4 -1
  6. package/dist/adapters/fivem/fivem-player-server.d.ts +1 -0
  7. package/dist/adapters/fivem/fivem-player-server.js +3 -0
  8. package/dist/adapters/node/node-engine-events.d.ts +1 -1
  9. package/dist/adapters/node/node-engine-events.js +4 -1
  10. package/dist/adapters/node/node-player-server.d.ts +1 -0
  11. package/dist/adapters/node/node-player-server.js +3 -0
  12. package/dist/runtime/client/system/processors/nui.processor.js +4 -3
  13. package/dist/runtime/server/bootstrap.js +99 -2
  14. package/dist/runtime/server/decorators/command.d.ts +1 -0
  15. package/dist/runtime/server/decorators/guard.d.ts +8 -6
  16. package/dist/runtime/server/decorators/guard.js +8 -6
  17. package/dist/runtime/server/decorators/onNet.d.ts +3 -3
  18. package/dist/runtime/server/runtime.d.ts +34 -1
  19. package/dist/runtime/server/runtime.js +1 -0
  20. package/dist/runtime/server/services/core/session-recovery.service.d.ts +39 -0
  21. package/dist/runtime/server/services/core/session-recovery.service.js +108 -0
  22. package/dist/runtime/server/services/index.d.ts +1 -0
  23. package/dist/runtime/server/services/index.js +3 -1
  24. package/dist/runtime/server/services/parallel/index.d.ts +1 -45
  25. package/dist/runtime/server/services/parallel/index.js +0 -44
  26. package/dist/runtime/server/services/parallel/native-worker.entry.d.ts +1 -0
  27. package/dist/runtime/server/services/parallel/native-worker.entry.js +12 -0
  28. package/dist/runtime/server/services/parallel/parallel-compute.service.d.ts +17 -17
  29. package/dist/runtime/server/services/parallel/parallel-compute.service.js +10 -3
  30. package/dist/runtime/server/services/parallel/types.d.ts +5 -2
  31. package/dist/runtime/server/services/parallel/worker-pool.d.ts +5 -9
  32. package/dist/runtime/server/services/parallel/worker-pool.js +167 -57
  33. package/dist/runtime/server/services/parallel/worker.d.ts +8 -5
  34. package/dist/runtime/server/services/parallel/worker.js +8 -5
  35. package/dist/runtime/server/services/services.register.js +2 -0
  36. package/dist/runtime/server/types/internal-events.d.ts +6 -0
  37. package/package.json +2 -2
package/README.md CHANGED
@@ -1,4 +1,12 @@
1
- # OpenCore Framework (v0.2.4) Open Stable beta
1
+ [![CI](https://github.com/newcore-network/opencore/actions/workflows/ci.yml/badge.svg)](https://github.com/newcore-network/opencore/actions/workflows/ci.yml)
2
+ ![npm](https://img.shields.io/npm/v/@open-core/framework?style=flat-square)
3
+ ![license](https://img.shields.io/github/license/newcore-network/opencore?style=flat-square)
4
+ ![typescript](https://img.shields.io/badge/TypeScript-first-blue?style=flat-square)
5
+ [![cli](https://img.shields.io/badge/CLI-opencore--cli-purple?style=flat-square)](https://opencorejs.dev/docs/cli/introduction)
6
+ [![website](https://img.shields.io/badge/web-opencorejs.dev-black?style=flat-square)](https://opencorejs.dev)
7
+
8
+
9
+ # OpenCore Framework - Open Stable beta
2
10
 
3
11
  OpenCore is a TypeScript multiplayer runtime framework targeting FiveM via an adapter.
4
12
 
@@ -11,6 +19,8 @@ It is not a gamemode or RP framework. It provides:
11
19
 
12
20
  License: MPL-2.0
13
21
 
22
+ [Discord Community](https://discord.gg/99g3FgvkPs) | [Website](https://opencorejs.dev) | [OpenCore CLI](https://github.com/newcore-network/opencore-cli)
23
+
14
24
  ## Scope
15
25
 
16
26
  This package (`@open-core/framework`) contains transversal infrastructure only.
@@ -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: (...args: any[]) => void): void;
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: (...args: any[]) => void): void;
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
- on(eventName, handler);
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);
@@ -12,4 +12,5 @@ export declare class FiveMPlayerServer extends IPlayerServer {
12
12
  getPing(playerSrc: string): number;
13
13
  getEndpoint(playerSrc: string): string;
14
14
  setRoutingBucket(playerSrc: string, bucket: number): void;
15
+ getConnectedPlayers(): string[];
15
16
  }
@@ -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: (...args: any[]) => void): void;
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
- this.eventEmitter.on(eventName, handler);
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
@@ -23,5 +23,6 @@ export declare class NodePlayerServer extends IPlayerServer {
23
23
  }): void;
24
24
  _wasDropped(playerSrc: string): boolean;
25
25
  setRoutingBucket(_playerSrc: string, _bucket: number): void;
26
+ getConnectedPlayers(): string[];
26
27
  _clear(): void;
27
28
  }
@@ -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 = [];
@@ -20,15 +20,16 @@ let NuiProcessor = class NuiProcessor {
20
20
  RegisterNuiCallbackType(metadata.eventName);
21
21
  on(`__cfx_nui:${metadata.eventName}`, async (data, cb) => {
22
22
  try {
23
- await handler(data);
24
- cb({ ok: true });
23
+ const result = await handler(data);
24
+ cb({ ok: true, data: result });
25
25
  }
26
26
  catch (error) {
27
27
  logger_1.loggers.nui.error(`NUI callback error`, {
28
28
  event: metadata.eventName,
29
29
  handler: handlerName,
30
30
  }, error);
31
- cb({ ok: false, error: String(error) });
31
+ const message = error instanceof Error ? error.message : String(error);
32
+ cb({ ok: false, error: message });
32
33
  }
33
34
  });
34
35
  logger_1.loggers.nui.debug(`Registered: ${metadata.eventName} -> ${handlerName}`);
@@ -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 ((_a = ctx.devMode) === null || _a === void 0 ? void 0 : _a.enabled) {
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 {};
@@ -30,20 +30,22 @@ export interface GuardOptions {
30
30
  *
31
31
  * @throws Error - If the method is invoked without a valid `Player` as the first argument.
32
32
  *
33
- * @example
34
33
  * ```ts
35
34
  * @Server.Controller()
36
- * export class FactionController {
35
+ * export class AdminController {
36
+ *
37
37
  * @Server.Guard({ permission: 'factions.manage' })
38
- * async createFaction(player: Server.Player, dto: CreateFactionDTO) {
38
+ * @Server.Command('newfaction', schema)
39
+ * async createFaction(player: Server.Player, dto: Infer<typeof schema>) {
39
40
  * return this.service.create(dto)
40
41
  * }
41
42
  *
42
43
  * @Server.Guard({ rank: 3 })
43
- * async promoteMember(player: Server.Player, memberID: string) {
44
- * return this.service.promote(player, memberID)
44
+ * @Server.Command('ban')
45
+ * async ban(player: Server.Player, targetID: string) {
46
+ * return this.service.ban(player, memberID)
45
47
  * }
46
- * }
48
+ }
47
49
  * ```
48
50
  */
49
51
  export declare function Guard(options: GuardOptions): (target: any, propertyKey: string, descriptor?: PropertyDescriptor) => PropertyDescriptor | undefined;
@@ -25,20 +25,22 @@ const principal_port_1 = require("../services/ports/principal.port");
25
25
  *
26
26
  * @throws Error - If the method is invoked without a valid `Player` as the first argument.
27
27
  *
28
- * @example
29
28
  * ```ts
30
29
  * @Server.Controller()
31
- * export class FactionController {
30
+ * export class AdminController {
31
+ *
32
32
  * @Server.Guard({ permission: 'factions.manage' })
33
- * async createFaction(player: Server.Player, dto: CreateFactionDTO) {
33
+ * @Server.Command('newfaction', schema)
34
+ * async createFaction(player: Server.Player, dto: Infer<typeof schema>) {
34
35
  * return this.service.create(dto)
35
36
  * }
36
37
  *
37
38
  * @Server.Guard({ rank: 3 })
38
- * async promoteMember(player: Server.Player, memberID: string) {
39
- * return this.service.promote(player, memberID)
39
+ * @Server.Command('ban')
40
+ * async ban(player: Server.Player, targetID: string) {
41
+ * return this.service.ban(player, memberID)
40
42
  * }
41
- * }
43
+ }
42
44
  * ```
43
45
  */
44
46
  function Guard(options) {
@@ -48,15 +48,15 @@ type ServerNetHandler<TArgs extends any[] = any[]> = (player: Player, ...args: T
48
48
  * export class ExampleController {
49
49
  * // Simple handler (no schema)
50
50
  * @Server.OnNet('example:ping')
51
- * ping(player: Player, message: string) { }
51
+ * ping(player: Server.Player, message: string) { }
52
52
  *
53
53
  * // With schema directly (recommended)
54
54
  * @Server.OnNet('example:data', PayloadSchema)
55
- * handleData(player: Player, data: Infer<typeof PayloadSchema>) { }
55
+ * handleData(player: Server.Player, data: Infer<typeof PayloadSchema>) { }
56
56
  *
57
57
  * // With options object (legacy)
58
58
  * @Server.OnNet('example:legacy', { schema: PayloadSchema })
59
- * handleLegacy(player: Player, data: Infer<typeof PayloadSchema>) { }
59
+ * handleLegacy(player: Server.Player, data: Infer<typeof PayloadSchema>) { }
60
60
  * }
61
61
  * ```
62
62
  */
@@ -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;
@@ -118,6 +118,7 @@ function createDefaultFeatures(mode) {
118
118
  export: false,
119
119
  scope,
120
120
  required: false,
121
+ recoveryOnRestart: true,
121
122
  },
122
123
  };
123
124
  }
@@ -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);