Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,44 @@ export class ExampleNetController {
- `@Throttle(limit, windowMs)`
- `@RequiresState({ missing: [...] })`

### Exports

`@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()`.

```ts
import { Controller, Export } from '@open-core/framework/server'
import { IExports } from '@open-core/framework/contracts/server'

@Controller()
export class DatabaseController {
@Export('pingDatabase')
async pingDatabase() {
return { success: true }
}
}

interface DatabaseExports {
pingDatabase(): Promise<{ success: boolean }>
}

class ExampleConsumer {
constructor(private readonly exportsService: IExports) {}

async ping() {
const database = await this.exportsService.waitForRemoteResource<DatabaseExports>('database', {
exportName: 'pingDatabase',
})

return database.pingDatabase()
}
}
```

Guidance:

- `getResource()` is for local/synchronous resolution used by framework internals.
- `waitForRemoteResource()` / `getRemoteResource()` are optional adapter utilities for explicit async resource-to-resource calls.

### Library events

Use library wrappers to emit domain events and `@OnLibraryEvent()` to observe them.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@open-core/framework",
"version": "1.0.8",
"version": "1.0.9",
"description": "Secure, event-driven TypeScript Framework & Runtime engine for CitizenFX (Cfx).",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
55 changes: 55 additions & 0 deletions src/adapters/contracts/IExports.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,59 @@
export abstract class IExports {
/**
* Registers a local export handler for the current resource.
*
* @remarks
* This is called by the framework during metadata processing when it discovers
* methods decorated with `@Export()`.
*/
abstract register(exportName: string, handler: (...args: unknown[]) => unknown): void

/**
* Resolves exports for a resource using the adapter's direct/local mechanism.
*
* @remarks
* Framework internals rely on this method remaining synchronous and side-effect free.
* Adapters should return `undefined` when the resource is not directly resolvable.
*/
abstract getResource<T = unknown>(resourceName: string): T | undefined

/**
* Returns an async proxy for resource exports when the adapter provides a remote helper layer.
*
* @remarks
* This is optional and should not change the semantics of `getResource()`.
* Consumers should treat methods on the returned proxy as async.
*/
getRemoteResource<T = unknown>(_resourceName: string): T {
throw new Error('[OpenCore] Remote exports are not supported by the active adapter.')
}

/**
* Calls a single exported method through the adapter's optional remote helper layer.
*/
callRemoteExport<TResult = unknown>(
_resourceName: string,
_exportName: string,
..._args: unknown[]
): Promise<TResult> {
return Promise.reject(
new Error('[OpenCore] Remote exports are not supported by the active adapter.'),
)
}

/**
* Waits until a resource exposes exports compatible with the adapter's remote helper layer.
*
* @param _options.exportName Optional export name that must be present before resolving.
* @param _options.timeoutMs Maximum time to wait before failing.
* @param _options.intervalMs Polling interval used by adapters that implement polling.
*/
waitForRemoteResource<T = unknown>(
_resourceName: string,
_options?: { exportName?: string; timeoutMs?: number; intervalMs?: number },
): Promise<T> {
return Promise.reject(
new Error('[OpenCore] Remote exports are not supported by the active adapter.'),
)
}
}
23 changes: 23 additions & 0 deletions src/adapters/node/node-exports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,29 @@ export class NodeExports implements IExports {
)
}

getRemoteResource<T = unknown>(_resourceName: string): T {
throw new Error('[OpenCore] Remote exports are not supported in Node.js runtime.')
}

callRemoteExport<TResult = unknown>(
_resourceName: string,
_exportName: string,
..._args: unknown[]
): Promise<TResult> {
return Promise.reject(
new Error('[OpenCore] Remote exports are not supported in Node.js runtime.'),
)
}

waitForRemoteResource<T = unknown>(
_resourceName: string,
_options?: { exportName?: string; timeoutMs?: number; intervalMs?: number },
): Promise<T> {
return Promise.reject(
new Error('[OpenCore] Remote exports are not supported in Node.js runtime.'),
)
}

/**
* Get all registered exports as an object
*/
Expand Down
27 changes: 26 additions & 1 deletion src/runtime/server/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -344,14 +344,39 @@ async function tryImportAutoLoad() {
try {
await import('./.opencore/autoload.server.controllers')
} catch (err) {
if (err instanceof Error && err.message.includes('Cannot find module')) {
if (isAutoloadModuleNotFound(err)) {
loggers.bootstrap.warn(`[Bootstrap] No server controllers autoload file found, skipping.`)
return
}

const message = err instanceof Error ? err.message : String(err)
loggers.bootstrap.error(`[Bootstrap] Failed to import server controllers autoload file.`, {
error: message,
})
throw err
}
}

function isAutoloadModuleNotFound(err: unknown): boolean {
if (!err || typeof err !== 'object') {
return false
}

const error = err as NodeJS.ErrnoException & { requireStack?: string[] }
const message = typeof error.message === 'string' ? error.message : ''
const requireStack = Array.isArray(error.requireStack) ? error.requireStack : []

if (error.code !== 'MODULE_NOT_FOUND' && !message.includes('Cannot find module')) {
return false
}

if (message.includes('autoload.server.controllers')) {
return true
}

return requireStack.some((entry) => entry.includes('autoload.server.controllers'))
}

/**
* Runs session recovery to restore sessions for players already connected.
*
Expand Down
Loading