Interfaces

Core Utilities

Learn about the essential utility functions provided by the core interface.

Module Context Functions

Antelopejs provides several core utility functions that power its interface-based architecture. These utilities enable features like metadata handling, interface implementation, and module awareness that are essential for building modular applications.

GetResponsibleModule

The GetResponsibleModule function is central to the module awareness capabilities of Antelopejs. It determines which module is responsible for the current code execution by analyzing the call stack.

Parameters

  • ignoreInterfaces (default: true): Whether to ignore interfaces when determining the responsible module
  • startFrame (default: 0): The starting frame in the stack trace to analyze

Return Value

Returns the module ID as a string, or undefined if no module is found.

Example

import { GetResponsibleModule } from '@ajs/core/beta';

// Get the ID of the module that's currently executing code
const currentModuleId = GetResponsibleModule();

// With custom parameters
const moduleId = GetResponsibleModule(false, 2); // includeInterfaces = false, startFrame = 2

// Common usage pattern for logging with module context
function logWithModuleContext(message: string) {
  const moduleId = GetResponsibleModule();
  console.log(`[${moduleId || 'unknown'}] ${message}`);
}

This function is primarily used internally by the proxy system to track which module registered callbacks or event handlers, but you can also use it directly when you need to obtain the current module context.

Interface Implementation Functions

InterfaceFunction

The InterfaceFunction utility creates a function that routes calls through an AsyncProxy, providing a more convenient way to work with asynchronous proxies. It's essentially a helper that wraps an AsyncProxy to create a more intuitive function interface, removing the need to use .call() when invoking the proxy.

Return Value

Returns a function that:

  • Takes the same parameters as the expected implementation
  • Returns a Promise resolving to the implementation's return value
  • Has a .proxy property containing the underlying AsyncProxy object

Example

import { InterfaceFunction } from '@ajs/core/beta';

// In an interface definition module
export const GetUserById = InterfaceFunction<(userId: string) => User>();
export const CreateUser = InterfaceFunction<(userData: UserInput) => User>();
export const DeleteUser = InterfaceFunction<(userId: string) => boolean>();

// In an implementation module
CreateUser.proxy.onCall(async (userData) => {
  // Implementation logic
  return newUser;
});

// In a consumer module
const user = await GetUserById('user123');
// The function can be called directly like a regular function
// Equivalent to using AsyncProxy directly: await GetUserById.proxy.call('user123');

// Another example with more complex data
const newUser = await CreateUser({ name: 'John Doe', email: '[email protected]' });

For more details on how AsyncProxy works under the hood, refer to the Proxies documentation.

ImplementInterface

The ImplementInterface function links a declared interface with its implementation, setting up the necessary proxies and event handlers to enable cross-module communication.

Parameters

  • declaration: The interface declaration to implement
  • implementation: The implementation of the interface (can be a Promise)

Return Value

Returns a Promise resolving to an object containing:

  • declaration: The resolved declaration
  • implementation: The resolved implementation

Example

import { ImplementInterface } from '@ajs/core/beta';
import * as userInterface from '@ajs.local/users/1';

// Implementation object matching the interface structure
const userImplementation = {
  getUser: async (id: string) => {
    // Implementation logic
    return { id, username: 'johndoe', email: '[email protected]', created: new Date() };
  },
  createUser: async (data) => {
    // Implementation logic
    return { id: 'new-id', ...data, created: new Date() };
  },
  // Other implementations...
};

// Connect the interface declaration with the implementation
await ImplementInterface(userInterface, userImplementation);

// Alternatively, in a module's construct function:
export async function construct() {
  // Import the interface declaration and implementation
  // You can also pass the import promises directly to ImplementInterface
  await ImplementInterface(
    import('@ajs.local/interfaces/users/1'),
    import('./implementations/users/1')
  );
}

For a complete guide on how interfaces and implementations work together, see the Export Interfaces chapter.

Metadata Handling

GetMetadata

The GetMetadata function retrieves or creates metadata associated with a target object. It's commonly used in decorator implementations for routing, parameter handling, and other reflection-based operations.

Parameters

  • target: The target object to get metadata for
  • meta: The metadata class with a static symbol key
  • inherit (default: true): Whether to inherit metadata from the prototype chain

Return Value

Returns an instance of the metadata class associated with the target.

Example

import { GetMetadata } from '@ajs/core/beta';
import { MakeMethodDecorator } from '@ajs/core/beta/decorators';

// Define a simple metadata class for controllers
class ControllerMeta {
  // Symbol key used for storage
  static key = Symbol('controller-metadata');

  // Store the base path for the controller
  path: string;

  // Track routes defined on this controller
  routes: Map<string, { method: string, path: string }> = new Map();

  constructor(target: any) {
    this.path = '';
  }
}

// Create a Controller class decorator
function Controller(path: string) {
  return function(target: any) {
    // Get or create metadata for the controller class
    const metadata = GetMetadata(target, ControllerMeta);
    // Set the controller's base path
    metadata.path = path;
    return target;
  };
}

// Create a GET method decorator
const Get = MakeMethodDecorator((target: any, key: string, descriptor: PropertyDescriptor, path?: string) => {
  // Get the controller's metadata
  const metadata = GetMetadata(target.constructor, ControllerMeta);
  // Register this method as a route
  metadata.routes.set(key, {
    method: 'GET',
    path: path || key
  });

  // Note: In a real implementation, we would need to register this route with a router or server
  // This example only demonstrates how to use metadata to store routing information

  return descriptor;
});

// Usage example
@Controller('/users')
class UserController {
  @Get()
  list() {
    return { users: [] };
  }

  @Get('/:id')
  getById(id: string) {
    return { id, name: 'John Doe' };
  }
}

// We can inspect the metadata later
const metadata = GetMetadata(UserController, ControllerMeta);
console.log(metadata.path); // '/users'
console.log(metadata.routes.get('list')); // { method: 'GET', path: 'list' }
console.log(metadata.routes.get('getById')); // { method: 'GET', path: '/:id' }

This pattern is commonly used in web frameworks to implement decorators. The metadata provides a way to store and retrieve information about classes and methods that can be processed at runtime.

Interface Discovery

Multiple modules can implement the same interface and version. In your application's antelope.json configuration, you can specify distinct identifiers for each implementation:

{
  "modules": {
    "mongodb": {
      "source": {
        "type": "package",
        "name": "@antelopejs/mongodb",
        "version": "0.0.1"
      },
      "config": {
        "url": "mongodb://localhost:27017"
      },
      "importOverrides": [
        {
          "interface": "database@beta",
          "source": "mongodb",
          "id": "main"
        }
      ]
    },
    "mongodb-2": {
      "source": {
        "type": "package",
        "name": "@antelopejs/mongodb",
        "version": "0.0.1"
      },
      "config": {
        "url": "mongodb://localhost:27018"
      },
      "importOverrides": [
        {
          "interface": "database@beta",
          "source": "mongodb-2",
          "id": "secondary"
        }
      ]
    }
  }
}

In this example, two modules are using the MongoDB implementation, but each connects to a different database instance. By specifying the id field in the importOverrides, you can later target a specific implementation using the utility functions below.

GetInterfaceInstances

The GetInterfaceInstances function retrieves all connections to implementations of a specified interface across the system. Note that the interfaceID is composed of the interface name and version in the format name@version.

Parameters

  • interfaceID: The ID of the interface to get instances for, in the format name@version

Return Value

Returns an array of interface connections, each containing:

  • id: Optional unique identifier for the connection
  • path: The path to the interface implementation

Example

import { GetInterfaceInstances } from '@ajs/core/beta';

// Get all instances of the 'database@beta' interface
const databaseInterfaceInstances = GetInterfaceInstances('database@beta');

// Connect to each database
for (const provider of databaseInterfaceInstances) {
  console.log(`Found database provider at: ${provider.path}`);
  console.log(`Provider ID: ${provider.id || 'default'}`);
  // Load and use the provider
}

GetInterfaceInstance

The GetInterfaceInstance function retrieves a specific connection to an implementation of the specified interface by ID.

Parameters

  • interfaceID: The ID of the interface to get an instance for, in the format name@version
  • connectionID: The ID of the specific connection to retrieve

Return Value

Returns the interface connection or undefined if not found, containing:

  • id: Unique identifier for the connection
  • path: The path to the interface implementation

Example

import { GetInterfaceInstance } from '@ajs/core/beta';

// Get a specific instance of the 'database@beta' interface
const primaryDatabaseProvider = GetInterfaceInstance('database@beta', 'main');

// Based on the antelope.json configuration above:
// Get the 'main' database provider
const mainProvider = GetInterfaceInstance('database@beta', 'main');
if (mainProvider) {
  // Load and use the specific provider connecting to localhost:27017
  const mainDb = await import(mainProvider.path);
  // Use the interface...
}

// Get the 'secondary' database provider
const secondaryProvider = GetInterfaceInstance('database@beta', 'secondary');
if (secondaryProvider) {
  // Load and use the specific provider connecting to localhost:27018
  const secondaryDb = await import(secondaryProvider.path);
  // Use the interface...
}

This approach allows your application to work with multiple implementations of the same interface simultaneously, each configured for different purposes.