Proxies
Overview
Proxies are the communication backbone of Antelopejs. They provide module-aware function calls, event broadcasting, and registration systems with automatic cleanup when modules unload. Every interface function, event, and registration in the framework is backed by a proxy.
The core tracks which module creates or attaches to each proxy. When a module is destroyed, all of its proxy bindings are automatically cleaned up. Automatic cleanup prevents memory leaks and stale references without requiring manual teardown code.
AsyncProxy
AsyncProxy manages async function calls between modules. A provider attaches an implementation with onCall, and consumers invoke it with call. If call is invoked before any implementation is attached, the request queues until one becomes available.
import { AsyncProxy } from "@antelopejs/interface-core";
const myProxy = new AsyncProxy<(id: string) => Promise<User>>();
// Provider: attach the implementation
myProxy.onCall(async (id: string) => {
return await fetchUser(id);
});
// Consumer: call the proxy
const user = await myProxy.call("user-123");
AsyncProxy Methods
| Method | Description |
|---|---|
onCall(callback, manualDetach?) | Attach a callback function. Queued calls execute once attached. |
call(...args) | Invoke the proxied function. Returns a Promise with the result. |
detach() | Manually detach the current callback. |
onCall a second time replaces the previous callback.EventProxy
EventProxy provides module-aware event broadcasting. Multiple handlers can register for the same event, and emit broadcasts to all of them. The core tracks which module registered each handler.
import { EventProxy } from "@antelopejs/interface-core";
const userCreated = new EventProxy<(user: User) => void>();
// Register a handler
const handler = (user: User) => {
console.log("User created:", user.name);
};
userCreated.register(handler);
// Emit to all handlers
userCreated.emit({ name: "Alice", id: "123" });
// Unregister a specific handler
userCreated.unregister(handler);
EventProxy Methods
| Method | Description |
|---|---|
register(callback) | Add an event handler. |
emit(...args) | Broadcast the event to all registered handlers. |
unregister(callback) | Remove a specific handler by reference. |
RegisteringProxy
RegisteringProxy manages registration-based systems where items are added and removed by ID. A provider sets up onRegister and onUnregister callbacks to react when consumers register or unregister items.
import { RegisteringProxy } from "@antelopejs/interface-core";
const routes = new RegisteringProxy<(id: string, path: string, handler: Function) => void>();
// Provider: set up registration/unregistration callbacks
routes.onRegister((id, path, handler) => {
// Add route to router
});
routes.onUnregister((id) => {
// Remove route from router
});
// Consumer: register items
routes.register("route-1", "/api/users", handleUsers);
routes.unregister("route-1");
RegisteringProxy Methods
| Method | Description |
|---|---|
onRegister(callback, manualDetach?) | Set the function called when an item is registered. |
onUnregister(callback) | Set the function called when an item is unregistered. |
register(id, ...args) | Register an item with a unique ID. |
unregister(id) | Unregister an item by its ID. |
detach() | Manually detach both the register and unregister callbacks. |
Automatic Cleanup
When a module is destroyed, the core automatically performs cleanup across all proxies:
- AsyncProxy — Detaches all callbacks attached by that module
- EventProxy — Unregisters all event handlers from that module
- RegisteringProxy — Removes all entries created by that module
This automatic cleanup is one of the key benefits of the proxy system. Modules do not need to write teardown logic for their proxy bindings — the core handles it based on ownership tracking.
Manual Detachment
In some cases, you need a proxy binding to survive module destruction. Pass true as the second argument to onCall() or onRegister() to opt out of automatic cleanup.
// This callback won't be auto-detached when the module is destroyed
myProxy.onCall(callback, true);
// This registration handler won't be auto-detached either
routes.onRegister(callback, true);
AsyncProxy Example
This example shows two modules communicating through an authentication interface. The auth module (producer) provides a login function, and a CLI module (consumer) calls it.
The interface file declares the proxy and exposes a wrapper function. InterfaceFunction creates an AsyncProxy internally and returns a callable function.
import { InterfaceFunction } from "@antelopejs/interface-core";
export interface Token {
accessToken: string;
expiresIn: number;
}
export namespace internal {
export const loginProxy = InterfaceFunction<
(username: string, password: string) => Promise<Token>
>();
}
export function Login(username: string, password: string): Promise<Token> {
return internal.loginProxy(username, password);
}
The producer module implements the login logic and wires it through ImplementInterface.
import type { Token } from "../interface";
export namespace internal {
export async function loginProxy(username: string, password: string): Promise<Token> {
// Validate credentials against your data store
if (username === "admin" && password === "secret") {
return { accessToken: "jwt-token-here", expiresIn: 3600 };
}
throw new Error("Invalid credentials");
}
}
import { ImplementInterface } from "@antelopejs/interface-core";
export async function construct() {
ImplementInterface(
await import("./interface"),
await import("./implementations")
);
}
The consumer module imports the wrapper function and calls it. The call queues until the auth module attaches its implementation, then resolves with the result.
import { Login } from "@your-org/auth-module/interface";
export async function start() {
const token = await Login("admin", "secret");
console.log("Authenticated, token expires in", token.expiresIn, "seconds");
}
EventProxy Example
This example shows a notification system where a user-management module emits an event whenever a user is created, and other modules listen for it.
The interface file declares the EventProxy and exposes helper functions for emitting and subscribing.
import { InterfaceFunction, EventProxy } from "@antelopejs/interface-core";
export interface User {
id: string;
name: string;
email: string;
}
export namespace internal {
export const createUserProxy = InterfaceFunction<
(name: string, email: string) => Promise<User>
>();
export const onUserCreated = new EventProxy<(user: User) => void>();
}
export function CreateUser(name: string, email: string): Promise<User> {
return internal.createUserProxy(name, email);
}
export function OnUserCreated(handler: (user: User) => void): void {
internal.onUserCreated.register(handler);
}
The producer implementation creates the user and emits the event. The EventProxy is not wired through ImplementInterface — the producer imports it directly from the interface and calls emit.
import type { User } from "../interface";
import { internal as iface } from "../interface";
export namespace internal {
export async function createUserProxy(name: string, email: string): Promise<User> {
const user: User = { id: crypto.randomUUID(), name, email };
// Persist the user, then broadcast the event
iface.onUserCreated.emit(user);
return user;
}
}
A consumer module registers a handler that runs each time a user is created. Multiple modules can register handlers on the same EventProxy.
import { OnUserCreated } from "@your-org/user-module/interface";
export function start() {
OnUserCreated((user) => {
console.log("Sending welcome email to", user.email);
});
}
When the email module is destroyed, the core automatically unregisters its handler from the EventProxy. No manual cleanup is needed.
RegisteringProxy Example
This example shows an HTTP module that allows other modules to register API routes dynamically. The HTTP module (producer) manages the route table, and feature modules (consumers) contribute routes.
The interface file declares a RegisteringProxy typed with an ID and the route metadata.
import { RegisteringProxy } from "@antelopejs/interface-core";
export interface RouteHandler {
method: "GET" | "POST" | "PUT" | "DELETE";
path: string;
handler: (req: any, res: any) => void | Promise<void>;
}
export namespace internal {
export const routesProxy = new RegisteringProxy<
(id: string, route: RouteHandler) => void
>();
}
export function RegisterRoute(id: string, route: RouteHandler): void {
internal.routesProxy.register(id, route);
}
export function UnregisterRoute(id: string): void {
internal.routesProxy.unregister(id);
}
The producer module sets up onRegister and onUnregister callbacks to react when routes are added or removed. For a RegisteringProxy, the implementation exports an object with register and unregister functions.
import type { RouteHandler } from "../interface";
const routeTable = new Map<string, RouteHandler>();
export namespace internal {
export const routesProxy = {
register(id: string, route: RouteHandler) {
routeTable.set(id, route);
console.log(`Route registered: ${route.method} ${route.path}`);
},
unregister(id: string) {
const route = routeTable.get(id);
if (route) {
routeTable.delete(id);
console.log(`Route removed: ${route.method} ${route.path}`);
}
},
};
}
import { ImplementInterface } from "@antelopejs/interface-core";
export async function construct() {
ImplementInterface(
await import("./interface"),
await import("./implementations")
);
}
Consumer modules register routes by calling the wrapper function. Each route gets a unique ID that can be used to remove it later.
import { RegisterRoute } from "@your-org/http-module/interface";
export function start() {
RegisterRoute("users-list", {
method: "GET",
path: "/api/users",
handler: (req, res) => {
res.end(JSON.stringify([{ id: "1", name: "Alice" }]));
},
});
RegisterRoute("users-create", {
method: "POST",
path: "/api/users",
handler: (req, res) => {
// Create user logic
res.end(JSON.stringify({ id: "2", name: "Bob" }));
},
});
}
When the users module is destroyed, the core automatically calls the unregister callback for each route that module registered. The HTTP module's route table stays clean without any manual teardown in the consumer.
See also
- Interfaces — Interface contracts and implementation
- Exporting Interfaces — How to publish interfaces from your module
Modules
Modules are the building blocks of an Antelopejs application — self-contained packages with a defined lifecycle, configuration, and interface bindings.
Configuration
Configure your Antelopejs project with antelope.config.ts — define modules, sources, environment overrides, logging, and per-environment settings.