Export Interfaces
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:
- AsyncProxy: Creates placeholder functions that will link to real code later
- RegisteringProxy: Lets modules register and unregister with each other
- 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:
- Backward Compatibility: New versions should extend functionality without breaking existing code
- Simple Versioning: Use straightforward numeric identifiers (1, 2, 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
orInterfaceFunction
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