Initial Commit
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
# `NativeBindingDetector` + `assertNoNativeBindings`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Hot reload is the heart of the API plugin system, but it has one
|
||||
non-negotiable boundary: native bindings (`.node` files compiled to a
|
||||
specific Node-API version) cannot be reloaded. Bun's ESM cache-bust trick
|
||||
re-imports a fresh JS module under a new specifier, but the underlying
|
||||
native dynamic library is loaded into the process's address space exactly
|
||||
once — re-running `dlopen` would either be ignored or, worse, corrupt the
|
||||
existing handle. The loader has to refuse the reload before it tries.
|
||||
|
||||
This module owns two pieces: the `NativeBindingDetector` function type
|
||||
(host-injected — `@suaveplan/api-host` ships a Bun-native implementation
|
||||
that walks the import subgraph), and the `assertNoNativeBindings` helper
|
||||
that throws the canonical `LOADER.NATIVE_BINDING_HELD` error when the
|
||||
detector reports a non-empty result. Per design doc §8.2: "Native bindings
|
||||
(`.node` files) cannot be reloaded; loader walks the dep graph at install
|
||||
time and refuses reload if any are present."
|
||||
|
||||
## Features
|
||||
|
||||
- Host-injected `NativeBindingDetector` function type — runtime-portable
|
||||
across Bun, Node, and a future Deno port.
|
||||
- `assertNoNativeBindings(bindings)` synchronous helper — throws
|
||||
`LOADER.NATIVE_BINDING_HELD` when the array is non-empty, no-ops
|
||||
otherwise.
|
||||
- `LoaderError` subclass widening the protected `SuaveplanError`
|
||||
constructor to public for the `LOADER.*` namespace.
|
||||
- Defensive copy on the offending paths: mutating the input after the throw
|
||||
cannot corrupt the error's `context.bindings`.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import {
|
||||
type NativeBindingDetector,
|
||||
assertNoNativeBindings,
|
||||
} from "@suaveplan/api-loader/native-binding-detector";
|
||||
|
||||
declare const detector: NativeBindingDetector;
|
||||
const bindings = await detector("file:///opt/plugin/index.js");
|
||||
assertNoNativeBindings(bindings); // safe pass-through when bindings.length === 0
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Combine with the resolver to wrap the install-time reload-eligibility check:
|
||||
|
||||
```ts
|
||||
import { assertNoNativeBindings } from "@suaveplan/api-loader/native-binding-detector";
|
||||
import type { ModuleResolver } from "@suaveplan/api-loader/resolver";
|
||||
|
||||
async function assertReloadable(
|
||||
resolver: ModuleResolver,
|
||||
specifier: string
|
||||
): Promise<void> {
|
||||
const bindings = await resolver.walkNativeBindings(specifier);
|
||||
assertNoNativeBindings(bindings);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
```ts
|
||||
export type NativeBindingDetector = (
|
||||
specifier: string
|
||||
) => Promise<readonly string[]>;
|
||||
|
||||
export class LoaderError extends SuaveplanError<LoaderCode> {
|
||||
constructor(
|
||||
code: LoaderCode,
|
||||
messageKey: string,
|
||||
options?: SuaveplanErrorOptions
|
||||
);
|
||||
}
|
||||
|
||||
export function assertNoNativeBindings(bindings: readonly string[]): void;
|
||||
```
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The thrown `LoaderError` attaches the offending paths via `context.bindings`,
|
||||
copying the input array (`[...bindings]`) so a caller mutating their array
|
||||
after the throw cannot retroactively change the error envelope. The
|
||||
copied form is `string[]` (mutable) but the type annotation on the input is
|
||||
`readonly string[]` — callers are encouraged to pass an immutable array,
|
||||
but the helper is defensive regardless.
|
||||
|
||||
The `messageKey` (`"loader.native_binding_held"`) is the i18n catalogue key
|
||||
used by `SuaveplanError`'s resolver; when the host's i18n catalogue is
|
||||
unwired, the resolver falls back to returning the key verbatim, which is
|
||||
adequate for the operator-facing logs the §8.7 failure-mode table cites.
|
||||
|
||||
`assertNoNativeBindings` is intentionally synchronous: it operates on the
|
||||
already-resolved output of `walkNativeBindings`, so adding an `await` would
|
||||
buy nothing. Callers usually wrap it in `async` code anyway because the
|
||||
detector itself is async.
|
||||
Reference in New Issue
Block a user