From a8d8b93cd1c574ecaf345a8e8cdb848fdfab0925 Mon Sep 17 00:00:00 2001 From: Victor Adossi Date: Sat, 24 Oct 2026 14:06:16 +0900 Subject: [PATCH 1/2] fix(bindgen): fix async import hang Co-authored-by: Yan Chen --- crates/js-component-bindgen/src/function_bindgen.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/crates/js-component-bindgen/src/function_bindgen.rs b/crates/js-component-bindgen/src/function_bindgen.rs index 75571f758..d262b9f39 100644 --- a/crates/js-component-bindgen/src/function_bindgen.rs +++ b/crates/js-component-bindgen/src/function_bindgen.rs @@ -2406,6 +2406,14 @@ impl Bindgen for FunctionBindgen<'_> { uwriteln!(self.src, "const started = task.enterSync();",); } + // A task that is only async for JSPI never publishes its completion + // promise, so the error must be thrown to reject the suspended guest. + let async_err_exit = if *async_ { + "return task.completionPromise();" + } else { + "throw err;" + }; + // Build the JS expression that calls the callee let (call_prefix, call_wrapper, call_err_cleanup) = if is_async || self.requires_async_porcelain @@ -2424,7 +2432,7 @@ impl Bindgen for FunctionBindgen<'_> { task.setErrored(err); task.reject(err); task.exit(); - return task.completionPromise(); + {async_err_exit} "# ), ) From b7d6167225cd05d9a8126c51109906be9e087d26 Mon Sep 17 00:00:00 2001 From: Victor Adossi Date: Sat, 24 Oct 2026 14:06:25 +0900 Subject: [PATCH 2/2] test(transpile): add regression test for failing JSPI import trap Co-authored-by: Yan Chen --- packages/jco-transpile/test/jspi.ts | 53 ++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/packages/jco-transpile/test/jspi.ts b/packages/jco-transpile/test/jspi.ts index 7f3d74691..d2ca703a7 100644 --- a/packages/jco-transpile/test/jspi.ts +++ b/packages/jco-transpile/test/jspi.ts @@ -3,7 +3,7 @@ import { mkdir, rm, symlink } from 'node:fs/promises'; import { fileURLToPath } from 'node:url'; -import { suite, test, assert } from 'vitest'; +import { suite, test, assert, expect } from 'vitest'; import { transpile } from '../src/index.js'; @@ -204,4 +204,55 @@ suite('Host Import Async (JSPI)', () => { await rm(outFile); } catch {} }); + + // A failing JSPI-suspended import must trap the guest and reject the + // export, rather than leave the guest suspended forever. + test.concurrent('Transpile async, import rejects (NodeJS, JSPI)', async () => { + if (typeof WebAssembly?.Suspending !== 'function') { + return; + } + const tmpDir = await getTmpDir(); + const modulesDir = resolve(tmpDir, 'node_modules', '@bytecodealliance'); + await mkdir(modulesDir, { recursive: true }); + await symlink( + fileURLToPath(new URL('../packages/preview2-shim', import.meta.url)), + resolve(modulesDir, 'preview2-shim'), + 'dir', + ); + + const inducedError = new Error('induced host rejection'); + const { instance, cleanup } = await setupAsyncTest({ + asyncMode: 'jspi', + component: { + name: 'async_call', + path: resolve('test/fixtures/components/runtime/async_call.component.wasm'), + imports: { + 'something:test/test-interface': { + callAsync: async () => { + throw inducedError; + }, + callSync: () => 'called sync', + }, + }, + }, + jco: { + transpile: { + extraArgs: { + asyncImports: ['something:test/test-interface#call-async'], + asyncExports: ['run-async'], + }, + }, + }, + }); + + try { + const exportCall = instance.runAsync(); + const timeout = new Promise((_, reject) => + setTimeout(() => reject(new Error('export call timed out')), 5_000), + ); + await expect(Promise.race([exportCall, timeout])).rejects.toBe(inducedError); + } finally { + await cleanup(); + } + }); });