fix(tsconnect/wasm): let the loader exit a runtime whose IPN failed

A rejected newIPN left the runtime blocked in main with nothing able to
release it: the IPN that owns shutdown was never built. The runtime, its
goroutines, and its scheduler work stayed live for a startup that failed.

Hand the loader a terminate function alongside the factory. Closing the
channel inside newIPN would not work, because main would return and the
runtime exit before makePromise delivered the rejection; leaving it to
the loader keeps the rejection first and the exit second.

jsIPN now holds that function instead of the channel, so shutdown and
startup failure release the runtime through one path.

Co-Authored-By: claude-opus-5 <noreply@anthropic.com>
This commit is contained in:
2026-08-30 01:06:16 +00:00
co-authored by Claude
parent 7ca658b028
commit 3c63a95446
2 changed files with 41 additions and 19 deletions
+18 -9
View File
@@ -19,14 +19,12 @@ export async function startIPN(
const name = `__tsconnectInit_${Math.random().toString(36).slice(2)}`
const globals = globalThis as Record<string, unknown>
const ready = new Promise<(config: IPNConfig) => Promise<IPN>>(
(resolve) => {
globals[name] = (newIPN: (config: IPNConfig) => Promise<IPN>) => {
delete globals[name]
resolve(newIPN)
}
const ready = new Promise<[NewIPN, Terminate]>((resolve) => {
globals[name] = (newIPN: NewIPN, terminate: Terminate) => {
delete globals[name]
resolve([newIPN, terminate])
}
)
})
go.env[INIT_CALLBACK_ENV] = name
const exited = go.run(instance).then(() => {
@@ -35,9 +33,20 @@ export async function startIPN(
throw new Error("Go runtime exited before the IPN was ready")
})
const newIPN = await Promise.race([ready, exited])
return newIPN(config)
const [newIPN, terminate] = await Promise.race([ready, exited])
try {
return await newIPN(config)
} catch (err) {
// Nothing was built, so nothing can shut the runtime down. Exit it here and
// wait for it, or the page keeps a blocked runtime for a failed startup.
terminate()
await exited.catch(() => {})
throw err
}
}
type NewIPN = (config: IPNConfig) => Promise<IPN>
type Terminate = () => void
/** Must match initCallbackEnv in wasm_js.go. */
const INIT_CALLBACK_ENV = "TSCONNECT_INIT_CALLBACK"