Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 50 additions & 7 deletions docs/2.proxies.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ const greeting = await proxy.call("Bob"); // "Hello, Bob!"

### `onCall(callback, manualDetach?)`

Attaches a callback function to the proxy. The proxy automatically tracks the calling module and detaches the callback when that module is unloaded. Pass `manualDetach: true` to disable automatic cleanup.
Attaches a callback function to the proxy. The proxy automatically tracks the calling module and detaches the callback when that module is unloaded. Pass `manualDetach: true` to disable automatic cleanup. Providers loaded by current AntelopeJS Core attach through an equivalent resolver-bound operation with explicit generation ownership.

When attachment occurs inside `RunWithModuleContext`, calls execute in the captured provider context rather than the consumer context. The captured module, owner generation, provider, and provider routes remain available across nested calls and `await`. Calls reject with `ModuleContextInvalidatedError` if the captured owner has been destroyed.
Resolver-bound providers do not need an ambient provider context: their implementation callbacks already close over imports selected for the provider module.

```ts
// Automatic cleanup (default) - detaches when the module unloads
Expand Down Expand Up @@ -129,18 +129,20 @@ routeRegistry.unregister("about");

Attaches the register callback. Any previously registered entries replay through this callback immediately. Automatic module-aware detachment applies unless `manualDetach` is `true`.

Register replay and direct registration execute in the provider context captured when the callback attaches.
Resolver-bound providers receive callbacks unchanged because imports inside those callbacks are already bound to the provider module.

### `onUnregister(callback)`

Attaches the unregister callback. This callback is detached at the same time as the register callback.

Unregistration executes in the context captured when this callback attaches.
Resolver-bound ownership applies equally to unregistration callbacks.

### `register(id, ...args)`

Registers an entry with the given identifier. If a register callback is attached, it executes immediately. Otherwise, the entry is stored and replayed when a callback is attached. The calling module is tracked for automatic cleanup.

Core selects the registering consumer's interface facade before module evaluation. Functions inside `args` therefore keep using that consumer's selected providers through their normal imports; registering proxies do not wrap those functions at invocation time.

### `unregister(id)`

Removes the entry with the given identifier and calls the unregister callback if one is attached.
Expand All @@ -149,6 +151,47 @@ Removes the entry with the given identifier and calls the unregister callback if

Manually removes both the register and unregister callbacks. Registered entries remain stored.

## Resolver-bound interface facades

Core resolves an interface package to a facade for each consuming module generation. The facade binds every exported `InterfaceFunction`, including functions nested in namespace objects, to the provider selected by that consumer's `importOverrides`. Application imports and call syntax do not change.

```ts
// Application code: unchanged, and already bound to this module's Auth provider.
import { ValidateRaw } from "@antelopejs/interface-auth";

const user = await ValidateRaw(token);
```

Classes, symbols, constants, proxy instances, and metadata objects keep their canonical identity. Only interface function exports receive generation-specific call functions. A stale facade rejects calls after its module generation is destroyed.

### Derived exports and registration APIs

Direct `InterfaceFunction` exports require no work from interface authors. An interface needs an internal `BuildInterfaceFacade` export only when another exported function closes over an interface function, or when a decorator/registration API must record generation ownership during module evaluation.

```ts
import type { InterfaceFacadeScope } from "@antelopejs/interface-core/facades";

export const Read = InterfaceFunction<() => string>("example.Read");
export const Registrations = new RegisteringProxy<
(id: string, handler: Handler) => void
>("example.Registrations");

export function BuildInterfaceFacade(
_scope: InterfaceFacadeScope,
facade: Record<string, unknown>,
) {
const boundRead = facade.Read as typeof Read;
const boundRegistrations = facade.Registrations as typeof Registrations;
return {
ReadUppercase: async () => (await boundRead()).toUpperCase(),
Register: (handler: Handler) =>
boundRegistrations.register(handler.id, handler),
};
}
```

The resolver builds and caches the facade before module evaluation. Interface builders derive helper functions and decorators from the bound values in `facade`; they never restore an ambient context around application callbacks.

## `ImplementInterface`

`ImplementInterface` connects an interface declaration to its implementation. It iterates over the declaration object and wires up each proxy to the corresponding implementation function.
Expand Down Expand Up @@ -239,7 +282,7 @@ const moduleId = GetResponsibleModule();

## `RunWithResponsibleModule`

`RunWithResponsibleModule` sets the responsible module explicitly for synchronous and asynchronous work. Proxy registrations made in the callback use this module directly instead of capturing and walking a stack. Nested contexts restore their parent when they complete, including when a callback throws.
`RunWithResponsibleModule` is a compatibility API that sets the responsible module explicitly for synchronous and asynchronous work. Proxy registrations made in the callback use this module directly instead of capturing and walking a stack. Nested contexts restore their parent when they complete, including when a callback throws.

```ts
import { RunWithResponsibleModule } from "@antelopejs/interface-core";
Expand All @@ -250,9 +293,9 @@ await RunWithResponsibleModule("my-module", async () => {
});
```

The module loader should wrap known module-owned entry points, including module evaluation and lifecycle hooks. Existing callers need no migration: outside an explicit context, `GetResponsibleModule` retains stack-based resolution as a backward-compatible fallback. Automatic proxy detachment and registration cleanup use the resolved module in both paths.
Current AntelopeJS Core does not wrap module evaluation, lifecycle hooks, or stored callbacks with this API. It resolves lexical interface facades before evaluating each module. Custom loaders and existing direct users can retain this API; outside an explicit context, `GetResponsibleModule` keeps stack-based resolution as a backward-compatible fallback.

Ownership contexts are scoped to a loaded module generation. Loaders that can overlap old and replacement instances should use `RunWithModuleContext` and provide a unique `owner` for every generation. `ModuleDestroyed` invalidates and cleans only the active event context's owner while preserving the existing module ID event contract. Detached asynchronous work from that owner then receives a `ModuleContextInvalidatedError`.
Resolver facades are scoped to a loaded module generation. `ModuleDestroyed` invalidates and cleans the exact owner emitted by Core. Calls retained from a destroyed generation then receive a `ModuleContextInvalidatedError`.

## Next steps

Expand Down
27 changes: 10 additions & 17 deletions docs/5.modules.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,25 +35,18 @@ loaded -> constructed -> active -> constructed -> loaded
| `active` | Module is fully started and providing services |
| `unknown` | Module status cannot be determined |

## Module execution context
## Advanced execution context APIs

`RunWithModuleContext` propagates module ownership and provider routing through synchronous and asynchronous work:
Provider selection is not an execution-context concern. Core resolves module-specific interface facades before module evaluation instead of installing an ambient context around lifecycle hooks or callbacks.

```ts
import { RunWithModuleContext } from "@antelopejs/interface-core/modules";

await RunWithModuleContext(
{
module: "search-provider",
owner: "search-provider#42",
provider: "search-provider",
providerRoutes: routes,
},
() => constructModule(),
);
```
| Situation | API to use |
| --- | --- |
| Application module lifecycle or ordinary interface call | None; Core binds the module's imports |
| Provider implementation attached with `ImplementInterface` | None; Core binds the provider's imports and ownership |
| Select a provider for an application module | Configure `importOverrides` |
| Inspect explicit ownership established with `RunWithResponsibleModule` | `GetModuleContext` |

`module` remains the stable public module ID. `owner` identifies one lifecycle generation and should be unique when old and replacement instances can overlap. Providers capture this full context when attaching callbacks. `GetModuleContext` returns the active context and throws `ModuleContextInvalidatedError` after its owner is destroyed.
`GetModuleContext` intentionally remains on the `/modules` subpath for ownership diagnostics. It does not select a provider. Application modules normally need neither API.

## Lifecycle events

Expand Down Expand Up @@ -101,7 +94,7 @@ Events.ModuleDestroyed.register((moduleId: string) => {
});
```

The event signature remains the module ID. When emitted inside `RunWithModuleContext`, cleanup targets that context's `owner`; without an explicit owner it retains the module-level behavior used by earlier releases.
The public event payload remains the module ID. Core also supplies the destroyed generation owner internally so cleanup cannot remove a replacement generation. An emitter that omits the owner retains the module-level cleanup behavior used by earlier releases.

## Management functions

Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@
"decorators": [
"dist/decorators.d.ts"
],
"facades": [
"dist/facades.d.ts"
],
"modules": [
"dist/modules.d.ts"
],
Expand Down
Loading
Loading