@open-core/framework 1.0.7 → 1.0.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.
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
  */
@@ -5,9 +5,11 @@ import { GLOBAL_CONTAINER, MetadataScanner } from '../../kernel/di/index';
5
5
  import { getLogLevel, LogLevelLabels, loggers } from '../../kernel/logger';
6
6
  import { createNodeServerAdapter } from './adapter/node-server-adapter';
7
7
  import { installServerAdapter } from './adapter/registry';
8
+ import { configureFrameworkEventBridge } from './bus/internal-event.bus';
8
9
  import { PrincipalProviderContract } from './contracts/index';
9
10
  import { getServerBinaryServiceRegistry } from './decorators/binaryService';
10
11
  import { getServerControllerRegistry } from './decorators/controller';
12
+ import { Players } from './ports/players.api-port';
11
13
  import { getFrameworkModeScope, setRuntimeContext, validateRuntimeOptions, } from './runtime';
12
14
  import { BinaryProcessManager } from './system/managers/binary-process.manager';
13
15
  import { PlayerPersistenceService } from './services/persistence.service';
@@ -116,6 +118,13 @@ export async function initServer(options, plugins) {
116
118
  // 1. Register Core Services (WorldContext, PlayerService, etc.)
117
119
  registerServicesServer(ctx);
118
120
  loggers.bootstrap.debug('Core services registered');
121
+ configureFrameworkEventBridge({
122
+ mode: ctx.mode,
123
+ engineEvents: GLOBAL_CONTAINER.resolve(IEngineEvents),
124
+ players: GLOBAL_CONTAINER.isRegistered(Players)
125
+ ? GLOBAL_CONTAINER.resolve(Players)
126
+ : undefined,
127
+ });
119
128
  // 2. Load Controllers (Framework & User controllers)
120
129
  // This is where user services get registered if they are decorated with @injectable()
121
130
  // and imported before init() or discovered here.
@@ -269,13 +278,32 @@ async function tryImportAutoLoad() {
269
278
  await import('./.opencore/autoload.server.controllers');
270
279
  }
271
280
  catch (err) {
272
- if (err instanceof Error && err.message.includes('Cannot find module')) {
281
+ if (isAutoloadModuleNotFound(err)) {
273
282
  loggers.bootstrap.warn(`[Bootstrap] No server controllers autoload file found, skipping.`);
274
283
  return;
275
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
+ });
276
289
  throw err;
277
290
  }
278
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
+ }
279
307
  /**
280
308
  * Runs session recovery to restore sessions for players already connected.
281
309
  *
@@ -1,6 +1,15 @@
1
- import { FrameworkEventsMap } from '../types/framework-events.types';
1
+ import { IEngineEvents } from '../../../adapters/contracts/IEngineEvents';
2
+ import { type FrameworkMode } from '../runtime';
3
+ import { Players } from '../ports/players.api-port';
4
+ import { type FrameworkEventsMap } from '../types/framework-events.types';
2
5
  type InternalEventName = keyof FrameworkEventsMap;
3
6
  type InternalEventHandler<E extends InternalEventName> = (payload: FrameworkEventsMap[E]) => void;
7
+ interface FrameworkEventBridgeConfig {
8
+ mode: FrameworkMode;
9
+ engineEvents?: IEngineEvents;
10
+ players?: Players;
11
+ }
4
12
  export declare function onFrameworkEvent<E extends InternalEventName>(event: E, handler: InternalEventHandler<E>): () => void;
13
+ export declare function configureFrameworkEventBridge(config: FrameworkEventBridgeConfig): void;
5
14
  export declare function emitFrameworkEvent<E extends InternalEventName>(event: E, payload: FrameworkEventsMap[E]): void;
6
15
  export {};
@@ -1,5 +1,10 @@
1
1
  import { loggers } from '../../../kernel/logger';
2
+ import { SYSTEM_EVENTS } from '../../shared/types/system-types';
2
3
  const handlers = {};
4
+ const bridgeListenerRegistrations = new WeakSet();
5
+ let bridgeConfig = {
6
+ mode: 'STANDALONE',
7
+ };
3
8
  export function onFrameworkEvent(event, handler) {
4
9
  let list = handlers[event];
5
10
  if (!list) {
@@ -13,7 +18,32 @@ export function onFrameworkEvent(event, handler) {
13
18
  list.splice(index, 1);
14
19
  };
15
20
  }
21
+ export function configureFrameworkEventBridge(config) {
22
+ bridgeConfig = config;
23
+ if (config.mode !== 'RESOURCE' || !config.engineEvents)
24
+ return;
25
+ if (bridgeListenerRegistrations.has(config.engineEvents))
26
+ return;
27
+ config.engineEvents.on(SYSTEM_EVENTS.framework.dispatch, (envelope) => {
28
+ if (bridgeConfig.mode !== 'RESOURCE')
29
+ return;
30
+ dispatchTransportFrameworkEvent(envelope.event, envelope.payload);
31
+ });
32
+ bridgeListenerRegistrations.add(config.engineEvents);
33
+ }
16
34
  export function emitFrameworkEvent(event, payload) {
35
+ dispatchLocalFrameworkEvent(event, payload);
36
+ if (bridgeConfig.mode !== 'CORE' || !bridgeConfig.engineEvents)
37
+ return;
38
+ const transportPayload = serializeFrameworkEvent(event, payload);
39
+ if (!transportPayload)
40
+ return;
41
+ bridgeConfig.engineEvents.emit(SYSTEM_EVENTS.framework.dispatch, {
42
+ event,
43
+ payload: transportPayload,
44
+ });
45
+ }
46
+ function dispatchLocalFrameworkEvent(event, payload) {
17
47
  const list = handlers[event];
18
48
  if (!list)
19
49
  return;
@@ -28,3 +58,68 @@ export function emitFrameworkEvent(event, payload) {
28
58
  }
29
59
  }
30
60
  }
61
+ function dispatchTransportFrameworkEvent(event, payload) {
62
+ const hydrated = hydrateFrameworkEvent(event, payload);
63
+ if (!hydrated)
64
+ return;
65
+ dispatchLocalFrameworkEvent(event, hydrated);
66
+ }
67
+ function serializeFrameworkEvent(event, payload) {
68
+ switch (event) {
69
+ case 'internal:playerSessionCreated':
70
+ case 'internal:playerSessionDestroyed':
71
+ return payload;
72
+ case 'internal:playerFullyConnected': {
73
+ const fullyConnectedPayload = payload;
74
+ return {
75
+ clientId: fullyConnectedPayload.player.clientID,
76
+ };
77
+ }
78
+ case 'internal:playerSessionRecovered': {
79
+ const recoveredPayload = payload;
80
+ return {
81
+ clientId: recoveredPayload.clientId,
82
+ license: recoveredPayload.license,
83
+ };
84
+ }
85
+ default:
86
+ return null;
87
+ }
88
+ }
89
+ function hydrateFrameworkEvent(event, payload) {
90
+ switch (event) {
91
+ case 'internal:playerSessionCreated':
92
+ case 'internal:playerSessionDestroyed':
93
+ return payload;
94
+ case 'internal:playerFullyConnected': {
95
+ const fullyConnectedPayload = payload;
96
+ const player = bridgeConfig.players?.getByClient(fullyConnectedPayload.clientId);
97
+ if (!player) {
98
+ loggers.eventBus.warn('Skipping framework event: player not found during hydration', {
99
+ event,
100
+ clientId: fullyConnectedPayload.clientId,
101
+ });
102
+ return null;
103
+ }
104
+ return { player };
105
+ }
106
+ case 'internal:playerSessionRecovered': {
107
+ const recoveredPayload = payload;
108
+ const player = bridgeConfig.players?.getByClient(recoveredPayload.clientId);
109
+ if (!player) {
110
+ loggers.eventBus.warn('Skipping framework event: player not found during hydration', {
111
+ event,
112
+ clientId: recoveredPayload.clientId,
113
+ });
114
+ return null;
115
+ }
116
+ return {
117
+ clientId: recoveredPayload.clientId,
118
+ license: recoveredPayload.license,
119
+ player,
120
+ };
121
+ }
122
+ default:
123
+ return null;
124
+ }
125
+ }
@@ -6,7 +6,23 @@ import { FrameworkEventsMap } from '../types/framework-events.types';
6
6
  * This decorator only stores metadata. The framework binds listeners during bootstrap by scanning
7
7
  * controller methods.
8
8
  *
9
- * The handler should accept the payload type corresponding to the event from {@link FrameworkEventsMap}.
9
+ * The handler receives the framework payload associated with the selected event from
10
+ * {@link FrameworkEventsMap}. Unlike {@link OnRuntimeEvent}, this payload is framework-defined
11
+ * and may include hydrated entities such as {@link Player}.
12
+ *
13
+ * Framework events are delivered:
14
+ * - locally in `STANDALONE`
15
+ * - locally inside `CORE`
16
+ * - from `CORE` to `RESOURCE` through the internal framework bridge
17
+ *
18
+ * For bridged events, the transport payload is serialized in `CORE` and then rehydrated in the
19
+ * receiving `RESOURCE`. That means handlers can keep using the same payload shape in every mode.
20
+ *
21
+ * Current built-in payloads:
22
+ * - `internal:playerSessionCreated`: `{ clientId, license }`
23
+ * - `internal:playerSessionDestroyed`: `{ clientId }`
24
+ * - `internal:playerFullyConnected`: `{ player }`
25
+ * - `internal:playerSessionRecovered`: `{ clientId, license, player }`
10
26
  *
11
27
  * @param event - Internal event name, strongly typed to {@link FrameworkEventsMap}.
12
28
  *
@@ -16,7 +32,7 @@ import { FrameworkEventsMap } from '../types/framework-events.types';
16
32
  * export class SystemController {
17
33
  * @Server.OnFrameworkEvent('internal:playerFullyConnected')
18
34
  * onPlayerConnected(payload: PlayerFullyConnectedPayload) {
19
- * console.log(`Player ${payload.player.session.clientId} connected`)
35
+ * console.log(`Player ${payload.player.clientID} connected`)
20
36
  * }
21
37
  * }
22
38
  * ```
@@ -6,7 +6,23 @@ import { METADATA_KEYS } from '../system/metadata-server.keys';
6
6
  * This decorator only stores metadata. The framework binds listeners during bootstrap by scanning
7
7
  * controller methods.
8
8
  *
9
- * The handler should accept the payload type corresponding to the event from {@link FrameworkEventsMap}.
9
+ * The handler receives the framework payload associated with the selected event from
10
+ * {@link FrameworkEventsMap}. Unlike {@link OnRuntimeEvent}, this payload is framework-defined
11
+ * and may include hydrated entities such as {@link Player}.
12
+ *
13
+ * Framework events are delivered:
14
+ * - locally in `STANDALONE`
15
+ * - locally inside `CORE`
16
+ * - from `CORE` to `RESOURCE` through the internal framework bridge
17
+ *
18
+ * For bridged events, the transport payload is serialized in `CORE` and then rehydrated in the
19
+ * receiving `RESOURCE`. That means handlers can keep using the same payload shape in every mode.
20
+ *
21
+ * Current built-in payloads:
22
+ * - `internal:playerSessionCreated`: `{ clientId, license }`
23
+ * - `internal:playerSessionDestroyed`: `{ clientId }`
24
+ * - `internal:playerFullyConnected`: `{ player }`
25
+ * - `internal:playerSessionRecovered`: `{ clientId, license, player }`
10
26
  *
11
27
  * @param event - Internal event name, strongly typed to {@link FrameworkEventsMap}.
12
28
  *
@@ -16,7 +32,7 @@ import { METADATA_KEYS } from '../system/metadata-server.keys';
16
32
  * export class SystemController {
17
33
  * @Server.OnFrameworkEvent('internal:playerFullyConnected')
18
34
  * onPlayerConnected(payload: PlayerFullyConnectedPayload) {
19
- * console.log(`Player ${payload.player.session.clientId} connected`)
35
+ * console.log(`Player ${payload.player.clientID} connected`)
20
36
  * }
21
37
  * }
22
38
  * ```
@@ -7,6 +7,19 @@ import type { RuntimeEventName } from '../../../adapters/contracts/runtime';
7
7
  * This decorator only stores metadata. During bootstrap, the framework scans controller
8
8
  * methods and binds handlers to runtime events.
9
9
  *
10
+ * The decorated method receives the raw runtime arguments emitted by the adapter for
11
+ * the selected event. OpenCore does not wrap these arguments into an object payload and
12
+ * does not automatically resolve a {@link Player} entity for you.
13
+ *
14
+ * Common server event signatures in the current runtime map:
15
+ * - `playerJoining`: `(clientId: number, identifiers?: Record<string, string>)`
16
+ * - `playerDropped`: `(clientId: number)`
17
+ * - `onServerResourceStop`: `(resourceName: string)`
18
+ * - `playerCommand`: runtime-specific raw arguments from the adapter
19
+ *
20
+ * If you need a framework-managed payload such as `{ player }`, use
21
+ * {@link OnFrameworkEvent} instead.
22
+ *
10
23
  * CitizenFX server event reference:
11
24
  * https://docs.fivem.net/docs/scripting-reference/events/server-events/
12
25
  *
@@ -17,8 +30,8 @@ import type { RuntimeEventName } from '../../../adapters/contracts/runtime';
17
30
  * @Server.Controller()
18
31
  * export class SessionController {
19
32
  * @Server.OnRuntimeEvent('playerJoining')
20
- * onPlayerJoining() {
21
- * // ...
33
+ * onPlayerJoining(clientId: number, identifiers?: Record<string, string>) {
34
+ * // Raw runtime arguments
22
35
  * }
23
36
  * }
24
37
  * ```
@@ -7,6 +7,19 @@ import { METADATA_KEYS } from '../system/metadata-server.keys';
7
7
  * This decorator only stores metadata. During bootstrap, the framework scans controller
8
8
  * methods and binds handlers to runtime events.
9
9
  *
10
+ * The decorated method receives the raw runtime arguments emitted by the adapter for
11
+ * the selected event. OpenCore does not wrap these arguments into an object payload and
12
+ * does not automatically resolve a {@link Player} entity for you.
13
+ *
14
+ * Common server event signatures in the current runtime map:
15
+ * - `playerJoining`: `(clientId: number, identifiers?: Record<string, string>)`
16
+ * - `playerDropped`: `(clientId: number)`
17
+ * - `onServerResourceStop`: `(resourceName: string)`
18
+ * - `playerCommand`: runtime-specific raw arguments from the adapter
19
+ *
20
+ * If you need a framework-managed payload such as `{ player }`, use
21
+ * {@link OnFrameworkEvent} instead.
22
+ *
10
23
  * CitizenFX server event reference:
11
24
  * https://docs.fivem.net/docs/scripting-reference/events/server-events/
12
25
  *
@@ -17,8 +30,8 @@ import { METADATA_KEYS } from '../system/metadata-server.keys';
17
30
  * @Server.Controller()
18
31
  * export class SessionController {
19
32
  * @Server.OnRuntimeEvent('playerJoining')
20
- * onPlayerJoining() {
21
- * // ...
33
+ * onPlayerJoining(clientId: number, identifiers?: Record<string, string>) {
34
+ * // Raw runtime arguments
22
35
  * }
23
36
  * }
24
37
  * ```
@@ -1,19 +1,65 @@
1
1
  import { Player } from '../entities';
2
+ /**
3
+ * Emitted when a player session is created in the framework session lifecycle.
4
+ */
2
5
  export interface PlayerSessionCreatedPayload {
3
6
  clientId: number;
4
7
  license: string | undefined;
5
8
  }
9
+ /**
10
+ * Emitted when a player session is destroyed in the framework session lifecycle.
11
+ */
6
12
  export interface PlayerSessionDestroyedPayload {
7
13
  clientId: number;
8
14
  }
15
+ /**
16
+ * Emitted when the framework considers the player fully connected and the runtime-specific
17
+ * {@link Player} entity can be consumed safely by application code.
18
+ */
9
19
  export interface PlayerFullyConnectedPayload {
10
20
  player: Player;
11
21
  }
22
+ /**
23
+ * Emitted when the framework recreates a player session after resource restart recovery.
24
+ */
12
25
  export interface PlayerSessionRecoveredPayload {
13
26
  clientId: number;
14
27
  player: Player;
15
28
  license: string | undefined;
16
29
  }
30
+ /**
31
+ * Serialized transport payload for `internal:playerFullyConnected` used across `CORE -> RESOURCE`.
32
+ */
33
+ export interface PlayerFullyConnectedTransportPayload {
34
+ clientId: number;
35
+ }
36
+ /**
37
+ * Serialized transport payload for `internal:playerSessionRecovered` used across `CORE -> RESOURCE`.
38
+ */
39
+ export interface PlayerSessionRecoveredTransportPayload {
40
+ clientId: number;
41
+ license: string | undefined;
42
+ }
43
+ /**
44
+ * Internal transport contract used by the framework bridge when delivering framework events
45
+ * between different server runtimes.
46
+ */
47
+ export type FrameworkTransportEventsMap = {
48
+ 'internal:playerSessionCreated': PlayerSessionCreatedPayload;
49
+ 'internal:playerSessionDestroyed': PlayerSessionDestroyedPayload;
50
+ 'internal:playerFullyConnected': PlayerFullyConnectedTransportPayload;
51
+ 'internal:playerSessionRecovered': PlayerSessionRecoveredTransportPayload;
52
+ };
53
+ /**
54
+ * Envelope emitted through the internal framework bridge.
55
+ */
56
+ export interface FrameworkEventEnvelope<E extends keyof FrameworkTransportEventsMap> {
57
+ event: E;
58
+ payload: FrameworkTransportEventsMap[E];
59
+ }
60
+ /**
61
+ * Public payload map consumed by {@link OnFrameworkEvent} and `onFrameworkEvent(...)`.
62
+ */
17
63
  export type FrameworkEventsMap = {
18
64
  'internal:playerSessionCreated': PlayerSessionCreatedPayload;
19
65
  'internal:playerSessionDestroyed': PlayerSessionDestroyedPayload;
@@ -16,6 +16,9 @@ export declare const SYSTEM_EVENTS: {
16
16
  readonly command: {
17
17
  readonly execute: `opencore:${string}:${string}`;
18
18
  };
19
+ readonly framework: {
20
+ readonly dispatch: `opencore:${string}:${string}`;
21
+ };
19
22
  readonly spawner: {
20
23
  readonly spawn: `opencore:${string}:${string}`;
21
24
  readonly teleport: `opencore:${string}:${string}`;
@@ -17,6 +17,9 @@ export const SYSTEM_EVENTS = {
17
17
  command: {
18
18
  execute: systemEvent('command', 'execute'),
19
19
  },
20
+ framework: {
21
+ dispatch: systemEvent('framework', 'dispatch'),
22
+ },
20
23
  spawner: {
21
24
  spawn: systemEvent('spawner', 'spawn'),
22
25
  teleport: systemEvent('spawner', 'teleport'),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-core/framework",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "description": "Secure, event-driven TypeScript Framework & Runtime engine for CitizenFX (Cfx).",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",