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>
53 lines
1.7 KiB
TypeScript
53 lines
1.7 KiB
TypeScript
// Copyright (c) Tailscale Inc & contributors
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
/**
|
|
* Starts a Go runtime and returns the single IPN it owns.
|
|
*
|
|
* The runtime does not publish its bridge on a global. It reads the name of a
|
|
* callback from its environment and invokes it once the bridge is ready, so
|
|
* this resolves on an explicit signal rather than on the Go scheduler having
|
|
* run far enough. The name is generated per runtime, so several runtimes can
|
|
* start in one page without racing each other.
|
|
*/
|
|
export async function startIPN(
|
|
go: Go,
|
|
instance: WebAssembly.Instance,
|
|
config: IPNConfig,
|
|
onExit: (reason: string) => void
|
|
): Promise<IPN> {
|
|
const name = `__tsconnectInit_${Math.random().toString(36).slice(2)}`
|
|
const globals = globalThis as Record<string, unknown>
|
|
|
|
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(() => {
|
|
delete globals[name]
|
|
onExit("Unexpected shutdown")
|
|
throw new Error("Go runtime exited before the IPN was ready")
|
|
})
|
|
|
|
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"
|