first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -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.