Concepts

Proxies

Proxies are the communication backbone of Antelopejs — AsyncProxy for function calls, EventProxy for broadcasting, and RegisteringProxy for registration systems.

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

MethodDescription
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.
An AsyncProxy accepts only one callback at a time. Calling 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

MethodDescription
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

MethodDescription
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.
RegisteringProxy is ideal for plugin-style systems — route registration, middleware stacks, template engines, and similar patterns where items come and go over the application lifetime.

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);
Use manual detachment sparingly. Bindings that outlive their module can cause unexpected behavior if the callback references resources that no longer exist.

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.

auth-module/src/interface/index.ts
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.

auth-module/src/implementations/index.ts
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");
  }
}
auth-module/src/index.ts
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.

cli-module/src/index.ts
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.

user-module/src/interface/index.ts
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.

user-module/src/implementations/index.ts
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.

email-module/src/index.ts
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.

http-module/src/interface/index.ts
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.

http-module/src/implementations/index.ts
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}`);
      }
    },
  };
}
http-module/src/index.ts
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.

users-module/src/index.ts
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