1 Commits
Author SHA1 Message Date
jochen 5d395c5473 One SDK per runtime: a bundle's SDK import resolves to the runtime's copy (hq issue 209)
A bundle carries its dependencies, the SDK among them; imported in-process that copy was a
second SDK with its own tool registry, so a bundle registered its tools into a list the
runtime never read and served nothing, silently. A resolve hook (module.registerHooks, in
thread; module.register is deprecated from Node 26) now sends every import of
@novox/mesh-sdk, from whichever bundle, to the runtime's own copy: one registry, one broker.
The test loads a bundle from a directory holding its own SDK copy and sees its tool served.
2026-10-03 12:49:41 +02:00
2 changed files with 23 additions and 41 deletions
+23 -3
View File
@@ -11,7 +11,7 @@
import { pathToFileURL } from "node:url";
import { resolve } from "node:path";
import { register } from "node:module";
import { registerHooks } from "node:module";
import { useBroker } from "@novox/mesh-sdk/messaging";
import { collectTools, toolKey, type ToolDefinition } from "@novox/mesh-sdk/tools";
import type { Broker } from "@novox/mesh-sdk/messaging";
@@ -319,9 +319,29 @@ async function serveClaimedSeats(
}
let sdkHooked = false;
/** Resolve `@novox/mesh-sdk` for every bundle to the copy this runtime imported (sdk-hooks.ts). */
const SDK = "@novox/mesh-sdk";
/**
* The one SDK in a node's runtime (novox/hq ADR 0175, 04-ISSUES/209).
*
* A bundle carries its own dependencies — the toolchain copies them in so a bundle starts anywhere
* (to-be 38 WP3) — and among them is a copy of the SDK. Imported in this process, that copy would be
* a second SDK: its own registry of tools, its own broker handle. A bundle calling registerModuleTools
* through it registers into a list this runtime never reads, and its tools are silently not served.
* So every import of the SDK, from whichever bundle, is resolved as if this runtime had written it:
* one registry, one broker — the runtime's. Everything else a bundle carries resolves from the
* bundle's own tree, as before. A launched bundle (ADR 0188) is another process and is untouched.
*
* Installed once, in-thread, before the first bundle is imported; the hook sees every import after.
*/
function oneSdk(): void {
if (sdkHooked) return;
sdkHooked = true;
register("./sdk-hooks.js", { parentURL: import.meta.url, data: { runtimeURL: import.meta.url } });
registerHooks({
resolve(specifier, context, next) {
if (specifier === SDK || specifier.startsWith(SDK + "/")) {
return next(specifier, { ...context, parentURL: import.meta.url });
}
return next(specifier, context);
},
});
}
-38
View File
@@ -1,38 +0,0 @@
// The one SDK in a node's runtime (novox/hq ADR 0175, 04-ISSUES/209).
//
// A bundle carries its own dependencies — the toolchain copies them in so a bundle starts anywhere
// (to-be 38 WP3) — and among them is a copy of @novox/mesh-sdk. Imported in this process, that copy
// would be a second SDK: its own registry of tools, its own broker handle. A bundle calling
// registerModuleTools through it registers into a list this runtime never reads, and its tools are
// silently not served. So every import of the SDK, from whichever bundle, is resolved as if this
// runtime had written it: one registry, one broker — the runtime's. Everything else a bundle
// carries resolves from the bundle's own tree, as before.
//
// Installed through module.register(), whose resolve hook sees every import that follows.
interface ResolveContext {
parentURL?: string;
conditions: string[];
importAttributes: Record<string, string>;
}
interface Resolved {
url: string;
format?: string | null;
shortCircuit?: boolean;
}
type NextResolve = (specifier: string, context?: Partial<ResolveContext>) => Promise<Resolved>;
const SDK = "@novox/mesh-sdk";
let runtimeURL = "";
/** Told, once, which file's tree holds the runtime's SDK. */
export function initialize(data: { runtimeURL: string }): void {
runtimeURL = data.runtimeURL;
}
export async function resolve(specifier: string, context: ResolveContext, next: NextResolve): Promise<Resolved> {
if (runtimeURL && (specifier === SDK || specifier.startsWith(SDK + "/"))) {
return next(specifier, { ...context, parentURL: runtimeURL });
}
return next(specifier, context);
}