Skip to content
Open
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
28 changes: 27 additions & 1 deletion rivetkit-typescript/packages/rivetkit/src/registry/native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,10 +224,36 @@ export async function loadAutoRuntime(
return (await loaders.loadWasm(config.wasm)).runtime;
}

let nativeError: unknown;
try {
return (await loaders.loadNative()).runtime;
} catch {
} catch (error) {
nativeError = error;
// Native is the expected runtime on a node-like host, so this is the
// actionable error even when the wasm fallback goes on to succeed.
// Discarding it hides causes such as a platform binding that npm
// silently skipped, which then resurfaces as an unrelated wasm error.
logger().warn({
msg: "native runtime failed to load; falling back to wasm",
error: stringifyError(error),
});
}

try {
return (await loaders.loadWasm(config.wasm)).runtime;
} catch (wasmError) {
// Report both, native first. The wasm failure on a node-like host is
// usually just its loader fetching over `file://`, which says nothing
// about why native was unavailable.
throw new RivetError(
"config",
"runtime_unavailable",
`RivetKit could not load a core runtime. Native runtime: ${stringifyError(nativeError)} Wasm runtime: ${stringifyError(wasmError)}`,
{
public: true,
statusCode: 500,
},
);
}
}

Expand Down
74 changes: 74 additions & 0 deletions rivetkit-typescript/packages/rivetkit/src/registry/runtime.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { describe, expect, test } from "vitest";
import type { RegistryConfig } from "./config";
import { loadAutoRuntime, type RuntimeLoaders } from "./native";
import {
type CoreRuntime,
normalizeRuntimeSqlExecuteResult,
type RuntimeSqlBindParam,
type RuntimeSqlBindParams,
Expand Down Expand Up @@ -48,3 +51,74 @@ describe("runtime SQL boundary", () => {
expect(normalizeRuntimeSqlExecuteResult(base)).toEqual(base);
});
});

describe("loadAutoRuntime failure reporting", () => {
const wasmRuntime = { kind: "wasm" } as unknown as CoreRuntime;
const nativeRuntime = { kind: "napi" } as unknown as CoreRuntime;
const config = {} as RegistryConfig;

function loaders(overrides: Partial<RuntimeLoaders>): RuntimeLoaders {
return {
detectHost: () => "node-like",
loadNative: async () => ({ runtime: nativeRuntime }) as never,
loadWasm: async () => ({ runtime: wasmRuntime }) as never,
...overrides,
};
}

test("prefers native when it loads", async () => {
const runtime = await loadAutoRuntime(config, loaders({}));
expect(runtime).toBe(nativeRuntime);
});

test("falls back to wasm when native fails", async () => {
const runtime = await loadAutoRuntime(
config,
loaders({
loadNative: async () => {
throw new Error("missing platform binding");
},
}),
);
expect(runtime).toBe(wasmRuntime);
});

test("reports the native cause when both runtimes fail", async () => {
// The native failure is the actionable one. Before this was reported,
// a skipped platform binding surfaced only as the wasm loader's
// unrelated `file://` fetch error.
const promise = loadAutoRuntime(
config,
loaders({
loadNative: async () => {
throw new Error(
"Cannot find module '@rivetkit/rivetkit-napi-linux-x64-musl'",
);
},
loadWasm: async () => {
throw new Error("fetch failed");
},
}),
);
await expect(promise).rejects.toThrow(/rivetkit-napi-linux-x64-musl/);
await expect(promise).rejects.toThrow(/fetch failed/);
});

test("uses wasm directly on an edge-like host without touching native", async () => {
let nativeCalls = 0;
const runtime = await loadAutoRuntime(
config,
loaders({
detectHost: () => "edge-like",
loadNative: async () => {
nativeCalls += 1;
throw new Error(
"native must not be attempted on edge hosts",
);
},
}),
);
expect(runtime).toBe(wasmRuntime);
expect(nativeCalls).toBe(0);
});
});
Loading