first commit
This commit is contained in:
107
docs/api-stack/api/api-loader/src/resolver/resolver.md
Normal file
107
docs/api-stack/api/api-loader/src/resolver/resolver.md
Normal file
@@ -0,0 +1,107 @@
|
||||
# `ModuleResolver`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`ModuleResolver` is the API plugin host's abstraction over `import()`. It
|
||||
imports a plugin entry under a cache-busted ESM specifier, returns an opaque
|
||||
`ModuleHandle`, and releases the host's strong reference when `dispose` is
|
||||
called so the previous module becomes eligible for GC. The contract lives
|
||||
here in `@suaveplan/api-loader`; concrete implementations (Bun-native ESM,
|
||||
ZIP-backed plugins, sandboxed worker proxy) land in `@suaveplan/api-host`
|
||||
Stage 2 and conform to the `runLoaderConformance` harness exported from
|
||||
`@suaveplan/api-loader/testing`. Design intent: design doc §8.1 and §8.2.
|
||||
|
||||
The contract is types-only. No runtime ships from this module beyond the
|
||||
type re-export shim — the type definitions exist so the lifecycle code in
|
||||
`api-host` and the spec for §8 have a single home that downstream tests can
|
||||
`expectTypeOf` against.
|
||||
|
||||
## Features
|
||||
|
||||
- `resolve({ specifier, version })` — imports the plugin entry under
|
||||
`bustSpecifier(specifier, version)` and returns a `ModuleHandle<M>`.
|
||||
- `dispose(handle)` — releases the host's strong ref to the handle so the
|
||||
previous module can be collected.
|
||||
- `walkNativeBindings(specifier)` — returns paths of `.node` files reachable
|
||||
from the entry's import subgraph; the host uses this at install to gate
|
||||
hot-reload eligibility.
|
||||
- Generic over the resolved module type `M` so consumers can pin a strict
|
||||
plugin-entry shape (e.g. `{ default: PluginFactory }`).
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import type { ModuleResolver } from "@suaveplan/api-loader/resolver";
|
||||
|
||||
declare const resolver: ModuleResolver<{ default: () => void }>;
|
||||
const handle = await resolver.resolve({
|
||||
specifier: "file:///opt/plugin/index.js",
|
||||
version: 1,
|
||||
});
|
||||
handle.module.default();
|
||||
await resolver.dispose(handle);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Combining `walkNativeBindings` with `assertNoNativeBindings` at install to
|
||||
gate hot-reload eligibility:
|
||||
|
||||
```ts
|
||||
import type { ModuleResolver } from "@suaveplan/api-loader/resolver";
|
||||
import { assertNoNativeBindings } from "@suaveplan/api-loader/native-binding-detector";
|
||||
|
||||
declare const resolver: ModuleResolver;
|
||||
const bindings = await resolver.walkNativeBindings(
|
||||
"file:///opt/plugin/index.js"
|
||||
);
|
||||
assertNoNativeBindings(bindings); // throws LOADER.NATIVE_BINDING_HELD if non-empty
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
```ts
|
||||
export interface ResolveOptions {
|
||||
readonly specifier: string;
|
||||
readonly version: number;
|
||||
}
|
||||
|
||||
export interface ModuleHandle<M = unknown> {
|
||||
readonly specifier: string;
|
||||
readonly version: number;
|
||||
readonly module: M;
|
||||
readonly resolvedAt: string;
|
||||
}
|
||||
|
||||
export interface ModuleResolver<M = unknown> {
|
||||
resolve(options: ResolveOptions): Promise<ModuleHandle<M>>;
|
||||
dispose(handle: ModuleHandle<M>): Promise<void>;
|
||||
walkNativeBindings(specifier: string): Promise<readonly string[]>;
|
||||
}
|
||||
|
||||
export type ModuleResolverFactory<M = unknown> = () => ModuleResolver<M>;
|
||||
```
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`resolve` returns the *evaluated namespace*, not a thenable wrapping it —
|
||||
the host's lifecycle FSM treats a successful return as a synchronous
|
||||
"module loaded" signal even though the call itself is async. Concrete
|
||||
implementations are free to memoise nothing (the cache-bust strategy in
|
||||
§8.2 makes memoisation incorrect) and to allocate a fresh `ModuleHandle` on
|
||||
every call.
|
||||
|
||||
`dispose` is `Promise<void>` rather than `void` because the in-host
|
||||
implementation may need to await a `gc()` round-trip in test environments;
|
||||
the contract surface does not assume synchronous release.
|
||||
|
||||
`walkNativeBindings` is `async` because the canonical implementation reads
|
||||
the dependency graph from a manifest or via a static traversal of `import`
|
||||
statements — both can involve disk I/O. The return type is
|
||||
`readonly string[]` (paths of `.node` files) rather than a single boolean so
|
||||
the operator-facing error envelope can show the exact bindings that blocked
|
||||
the reload.
|
||||
Reference in New Issue
Block a user