Interfaces

Export Interfaces

Learn how to create and share interfaces between your modules. We'll cover how to organize, version, and implement them properly.

Exporting Interfaces

Interfaces are how modules talk to each other in Antelopejs. Think of them as contracts that define what one module expects from another. Let's see how to create and use them.

Interface Organization

Directory Structure

Put your interfaces in folders like this:

src/
└── interfaces/
    └── example/           # Interface name
        ├── 1/             # Version 1
        │   └── index.ts   # Main interface code
        └── 2/             # Version 2
            ├── index.ts   # Main interface code
            └── types.ts   # Extra types

Configuration in package.json

Tell your module where to find interfaces in the antelopeJs section of package.json:

{
  "name": "my-module",
  "version": "1.0.0",
  "antelopeJs": {
    "exportsPath": "dist/interfaces"
  }
}

You can also set this with a simple command:

ajs module exports set dist/interfaces

Creating Interfaces

Interfaces define how modules talk to each other using functions, types, and events.

Using Proxy Objects

Antelopejs gives you three main ways to create interfaces:

  1. AsyncProxy: Creates placeholder functions that will link to real code later
  2. RegisteringProxy: Lets modules register and unregister with each other
  3. EventProxy: Helps modules send and receive events

Interface Example

Here's a basic example of an interface for a user service using the InterfaceFunction helper (which wraps AsyncProxy):

// src/interfaces/users/1/index.ts
import { InterfaceFunction, RegisteringProxy } from "@ajs/core/beta";

// Define what a user looks like
export interface User {
  id: string;
  username: string;
  email: string;
  created: Date;
}

// Define functions other modules can use
export const getUser =
  InterfaceFunction<(id: string) => Promise<User | null>>();
export const createUser =
  InterfaceFunction<(data: Omit<User, "id" | "created">) => Promise<User>>();
export const updateUser =
  InterfaceFunction<(id: string, data: Partial<User>) => Promise<User>>();
export const deleteUser = InterfaceFunction<(id: string) => Promise<boolean>>();

// Let modules listen for user events
export const userEvents = new RegisteringProxy<
  (
    id: string,
    listener: (event: "created" | "updated" | "deleted", user: User) => void
  ) => void
>();

Implementing Interfaces

Once you've defined an interface, you need to make it actually do something.

Implementation Example

Here's how to implement our user interface:

// src/implementations/users/1/index.ts
import { User } from "@ajs.local/interfaces/users/1";

// Store users in memory for this example
const users = new Map<string, User>();

// Get a user by ID
export const getUser = async (id: string): Promise<User | null> => {
  return users.get(id) || null;
};

// Create a new user
export const createUser = async (
  data: Omit<User, "id" | "created">
): Promise<User> => {
  const id = Math.random().toString(36).substring(2, 15);
  const user: User = {
    id,
    ...data,
    created: new Date(),
  };
  users.set(id, user);

  // Tell listeners a user was created
  notifyListeners("created", user);
  return user;
};

// Update a user
export const updateUser = async (
  id: string,
  data: Partial<User>
): Promise<User> => {
  const existing = users.get(id);
  if (!existing) {
    throw new Error(`User not found: ${id}`);
  }

  const updated = { ...existing, ...data };
  users.set(id, updated);

  // Tell listeners a user was updated
  notifyListeners("updated", updated);
  return updated;
};

// Delete a user
export const deleteUser = async (id: string): Promise<boolean> => {
  const user = users.get(id);
  if (!user) return false;

  const success = users.delete(id);
  if (success) {
    // Tell listeners a user was deleted
    notifyListeners("deleted", user);
  }
  return success;
};

// Keep track of listeners
const listeners = new Map<
  string,
  (event: "created" | "updated" | "deleted", user: User) => void
>();

// Handle registering and unregistering listeners
export const userEvents = {
  register: (
    id: string,
    listener: (event: "created" | "updated" | "deleted", user: User) => void
  ) => {
    listeners.set(id, listener);
  },
  unregister: (id: string) => {
    listeners.delete(id);
  },
};

// Helper function to notify all listeners
function notifyListeners(event: "created" | "updated" | "deleted", user: User) {
  for (const listener of listeners.values()) {
    listener(event, user);
  }
}

Connecting Interface with Implementation

In your module's main file, connect everything together:

// src/index.ts
import { ImplementInterface } from "@ajs/core/beta";

export async function construct() {
  // Connect the interface with its implementation
  await ImplementInterface(
    import("@ajs.local/interfaces/users/1"),
    import("./implementations/users/1")
  );
}

export function start() {
  // Start any processes here
}

export function stop() {
  // Stop any processes here
}

export function destroy() {
  // Clean up resources here
}

Proxy Types Explained

AsyncProxy

AsyncProxy creates a function placeholder that will be linked to an implementation later:

  • Queues function calls made before implementation is attached
  • Automatically forwards calls to the implementation once attached
  • Detaches when the implementing module is unloaded

RegisteringProxy

RegisteringProxy creates registration/unregistration function pairs:

  • Allows consumers to register with a service and automatically unregister
  • Maintains a registry of registered consumers
  • Cleans up when modules are unloaded

EventProxy

EventProxy creates an event system for communication:

  • Allows broadcasting events to multiple listeners
  • Automatically cleans up handlers from unloaded modules
  • Provides a publish/subscribe pattern for module communication

Interface Versioning

Interface versioning follows these principles:

  1. Backward Compatibility: New versions should extend functionality without breaking existing code
  2. Simple Versioning: Use straightforward numeric identifiers (1, 2, 3)
  3. Explicit Imports: Always specify the exact version when importing

Version Migration Example

// Using version 1
import { getUser } from "@ajs/users/1";

// Later migrating to version 2 which adds more features
import { getUser, searchUsers } from "@ajs/users/2";

Best Practices

Keep Interfaces Clean

Interfaces should define what functionality is available with minimal implementation details:

  • Generic Implementation Only: Interfaces can contain generic implementation logic, but specific implementations should be defined as proxies
  • Minimal External Dependencies: Keep external dependencies to a minimum in interface files
  • No Local Module Imports: Avoid using @ajs.local imports inside interfaces as the resulting typing will not work for modules importing it
  • Clear Documentation: Document the purpose and usage of each interface function

Use Appropriate Proxies

Choose the right proxy for each use case:

  • Use AsyncProxy or InterfaceFunction for standard function calls
  • Use RegisteringProxy for registration systems (listeners, handlers, etc.)
  • Use EventProxy for event broadcasting systems

Clean Up Resources

Handle module lifecycle properly:

  • Implement the destroy function to clean up resources
  • Use core lifecycle events to handle module unloading
  • Avoid storing references to objects from other modules without cleanup logic